diff --git a/.gitattributes b/.gitattributes index e3fb061bbc..e1225939b1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,12 @@ mobile/openapi/**/*.dart linguist-generated=true mobile/lib/**/*.g.dart -diff -merge mobile/lib/**/*.g.dart linguist-generated=true +mobile/android/**/*.g.kt -diff -merge +mobile/android/**/*.g.kt linguist-generated=true + +mobile/ios/**/*.g.swift -diff -merge +mobile/ios/**/*.g.swift linguist-generated=true + mobile/lib/**/*.drift.dart -diff -merge mobile/lib/**/*.drift.dart linguist-generated=true diff --git a/.github/.nvmrc b/.github/.nvmrc index 32f8c50de0..8e35034890 100644 --- a/.github/.nvmrc +++ b/.github/.nvmrc @@ -1 +1 @@ -24.13.1 +24.14.1 diff --git a/.github/workflows/auto-close.yml b/.github/workflows/auto-close.yml new file mode 100644 index 0000000000..aa5d41ff98 --- /dev/null +++ b/.github/workflows/auto-close.yml @@ -0,0 +1,148 @@ +name: Auto-close PRs + +on: + pull_request_target: # zizmor: ignore[dangerous-triggers] + types: [opened, edited, labeled] + +permissions: {} + +jobs: + parse_template: + runs-on: ubuntu-latest + if: ${{ github.event.action != 'labeled' && github.event.pull_request.head.repo.fork == true }} + permissions: + contents: read + outputs: + uses_template: ${{ steps.check.outputs.uses_template }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: .github/pull_request_template.md + sparse-checkout-cone-mode: false + persist-credentials: false + + - name: Check required sections + id: check + env: + BODY: ${{ github.event.pull_request.body }} + run: | + OK=true + while IFS= read -r header; do + printf '%s\n' "$BODY" | grep -qF "$header" || OK=false + done < <(sed '//d' .github/pull_request_template.md | grep "^## ") + echo "uses_template=$OK" | tee --append "$GITHUB_OUTPUT" + + close_template: + runs-on: ubuntu-latest + needs: parse_template + if: >- + ${{ + needs.parse_template.outputs.uses_template == 'false' + && github.event.pull_request.state != 'closed' + && !contains(github.event.pull_request.labels.*.name, 'auto-closed:template') + }} + permissions: + pull-requests: write + steps: + - name: Comment and close + env: + GH_TOKEN: ${{ github.token }} + NODE_ID: ${{ github.event.pull_request.node_id }} + run: | + gh api graphql \ + -f prId="$NODE_ID" \ + -f body="This PR has been automatically closed as the description doesn't follow [our template](https://github.com/immich-app/immich/blob/main/.github/pull_request_template.md). After you edit it to match the template, the PR will automatically be reopened." \ + -f query=' + mutation CommentAndClosePR($prId: ID!, $body: String!) { + addComment(input: { + subjectId: $prId, + body: $body + }) { + __typename + } + closePullRequest(input: { + pullRequestId: $prId + }) { + __typename + } + }' + + - name: Add label + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "auto-closed:template" + + close_llm: + runs-on: ubuntu-latest + if: ${{ github.event.action == 'labeled' && github.event.label.name == 'auto-closed:llm' }} + permissions: + pull-requests: write + steps: + - name: Comment and close + env: + GH_TOKEN: ${{ github.token }} + NODE_ID: ${{ github.event.pull_request.node_id }} + run: | + gh api graphql \ + -f prId="$NODE_ID" \ + -f body="Thank you for your interest in contributing to Immich! Unfortunately this PR looks like it was generated using an LLM. As noted in our [CONTRIBUTING.md](https://github.com/immich-app/immich/blob/main/CONTRIBUTING.md#use-of-generative-ai), we request that you don't use LLMs to generate PRs as those are not a good use of maintainer time." \ + -f query=' + mutation CommentAndClosePR($prId: ID!, $body: String!) { + addComment(input: { + subjectId: $prId, + body: $body + }) { + __typename + } + closePullRequest(input: { + pullRequestId: $prId + }) { + __typename + } + }' + + reopen: + runs-on: ubuntu-latest + needs: parse_template + if: >- + ${{ + needs.parse_template.outputs.uses_template == 'true' + && github.event.pull_request.state == 'closed' + && contains(github.event.pull_request.labels.*.name, 'auto-closed:template') + }} + permissions: + pull-requests: write + steps: + - name: Remove template label + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --remove-label "auto-closed:template" || true + + - name: Check for remaining auto-closed labels + id: check_labels + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + REMAINING=$(gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" --json labels \ + --jq '[.labels[].name | select(startswith("auto-closed:"))] | length') + echo "remaining=$REMAINING" | tee --append "$GITHUB_OUTPUT" + + - name: Reopen PR + if: ${{ steps.check_labels.outputs.remaining == '0' }} + env: + GH_TOKEN: ${{ github.token }} + NODE_ID: ${{ github.event.pull_request.node_id }} + run: | + gh api graphql \ + -f prId="$NODE_ID" \ + -f query=' + mutation ReopenPR($prId: ID!) { + reopenPullRequest(input: { + pullRequestId: $prId + }) { + __typename + } + }' diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 44645c1e1b..c2a3918cfe 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -51,14 +51,14 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 + uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -79,7 +79,7 @@ jobs: steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -103,7 +103,7 @@ jobs: - name: Restore Gradle Cache id: cache-gradle-restore - uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: | ~/.gradle/caches @@ -114,14 +114,14 @@ jobs: key: build-mobile-gradle-${{ runner.os }}-main - name: Setup Flutter SDK - uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0 + uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 with: channel: 'stable' flutter-version-file: ./mobile/pubspec.yaml cache: true - name: Setup Android SDK - uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2 + uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 with: packages: '' @@ -153,14 +153,14 @@ jobs: fi - name: Publish Android Artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: release-apk-signed path: mobile/build/app/outputs/flutter-apk/*.apk - name: Save Gradle Cache id: cache-gradle-save - uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 if: github.ref == 'refs/heads/main' with: path: | @@ -185,13 +185,13 @@ jobs: run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref || github.sha }} persist-credentials: false - name: Setup Flutter SDK - uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2 + uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 with: channel: 'stable' flutter-version-file: ./mobile/pubspec.yaml @@ -210,7 +210,7 @@ jobs: working-directory: ./mobile - name: Setup Ruby - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 with: ruby-version: '3.3' bundler-cache: true @@ -291,7 +291,7 @@ jobs: security delete-keychain build.keychain || true - name: Upload IPA artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ios-release-ipa path: mobile/ios/Runner.ipa diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml index 3de4676622..e093cf9bf0 100644 --- a/.github/workflows/cache-cleanup.yml +++ b/.github/workflows/cache-cleanup.yml @@ -19,7 +19,7 @@ jobs: actions: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml index b725b45f6c..3b3eb774cb 100644 --- a/.github/workflows/check-openapi.yml +++ b/.github/workflows/check-openapi.yml @@ -24,7 +24,7 @@ jobs: persist-credentials: false - name: Check for breaking API changes - uses: oasdiff/oasdiff-action/breaking@748daafaf3aac877a36307f842a48d55db938ac8 # v0.0.31 + uses: oasdiff/oasdiff-action/breaking@e6faebce24cf20ac38653d0d2c7f4aa80aaafc79 # v0.0.38 with: base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json revision: open-api/immich-openapi-specs.json diff --git a/.github/workflows/check-pr-template.yml b/.github/workflows/check-pr-template.yml deleted file mode 100644 index 4dcdd20f72..0000000000 --- a/.github/workflows/check-pr-template.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: Check PR Template - -on: - pull_request_target: # zizmor: ignore[dangerous-triggers] - types: [opened, edited] - -permissions: {} - -jobs: - parse: - runs-on: ubuntu-latest - if: ${{ github.event.pull_request.head.repo.fork == true }} - permissions: - contents: read - outputs: - uses_template: ${{ steps.check.outputs.uses_template }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - sparse-checkout: .github/pull_request_template.md - sparse-checkout-cone-mode: false - persist-credentials: false - - - name: Check required sections - id: check - env: - BODY: ${{ github.event.pull_request.body }} - run: | - OK=true - while IFS= read -r header; do - printf '%s\n' "$BODY" | grep -qF "$header" || OK=false - done < <(sed '//d' .github/pull_request_template.md | grep "^## ") - echo "uses_template=$OK" >> "$GITHUB_OUTPUT" - - act: - runs-on: ubuntu-latest - needs: parse - permissions: - pull-requests: write - steps: - - name: Close PR - if: ${{ needs.parse.outputs.uses_template == 'false' && github.event.pull_request.state != 'closed' }} - env: - GH_TOKEN: ${{ github.token }} - NODE_ID: ${{ github.event.pull_request.node_id }} - run: | - gh api graphql \ - -f prId="$NODE_ID" \ - -f body="This PR has been automatically closed as the description doesn't follow our template. After you edit it to match the template, the PR will automatically be reopened." \ - -f query=' - mutation CommentAndClosePR($prId: ID!, $body: String!) { - addComment(input: { - subjectId: $prId, - body: $body - }) { - __typename - } - closePullRequest(input: { - pullRequestId: $prId - }) { - __typename - } - }' - - - name: Reopen PR (sections now present, PR closed) - if: ${{ needs.parse.outputs.uses_template == 'true' && github.event.pull_request.state == 'closed' }} - env: - GH_TOKEN: ${{ github.token }} - NODE_ID: ${{ github.event.pull_request.node_id }} - run: | - gh api graphql \ - -f prId="$NODE_ID" \ - -f query=' - mutation ReopenPR($prId: ID!) { - reopenPullRequest(input: { - pullRequestId: $prId - }) { - __typename - } - }' diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index d3eb66810e..2a334af89d 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -31,7 +31,7 @@ jobs: working-directory: ./cli steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -42,7 +42,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -71,7 +71,7 @@ jobs: steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -83,13 +83,13 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Login to GitHub Container Registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 if: ${{ !github.event.pull_request.head.repo.fork }} with: registry: ghcr.io @@ -104,7 +104,7 @@ jobs: - name: Generate docker image tags id: metadata - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 with: flavor: | latest=false @@ -115,7 +115,7 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'release' }} - name: Build and push image - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: file: cli/Dockerfile platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/close-duplicates.yml b/.github/workflows/close-duplicates.yml index 1b18c0c5e1..839e5b3ceb 100644 --- a/.github/workflows/close-duplicates.yml +++ b/.github/workflows/close-duplicates.yml @@ -35,7 +35,7 @@ jobs: needs: [get_body, should_run] if: ${{ needs.should_run.outputs.should_run == 'true' }} container: - image: ghcr.io/immich-app/mdq:main@sha256:4f9860d04c88f7f87861f8ee84bfeedaec15ed7ca5ca87bc7db44b036f81645f + image: ghcr.io/immich-app/mdq:main@sha256:557cca601891b8b7d78b940071d35aaf7aaeb9b327d19b22cf282118edbc5272 outputs: checked: ${{ steps.get_checkbox.outputs.checked }} steps: diff --git a/.github/workflows/close-llm-pr.yml b/.github/workflows/close-llm-pr.yml deleted file mode 100644 index 511d5c7f55..0000000000 --- a/.github/workflows/close-llm-pr.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Close LLM-generated PRs - -on: - pull_request_target: - types: [labeled] - -permissions: {} - -jobs: - comment_and_close: - runs-on: ubuntu-latest - if: ${{ github.event.label.name == 'llm-generated' }} - permissions: - pull-requests: write - steps: - - name: Comment and close - env: - GH_TOKEN: ${{ github.token }} - NODE_ID: ${{ github.event.pull_request.node_id }} - run: | - gh api graphql \ - -f prId="$NODE_ID" \ - -f body="Thank you for your interest in contributing to Immich! Unfortunately this PR looks like it was generated using an LLM. As noted in our [CONTRIBUTING.md](https://github.com/immich-app/immich/blob/main/CONTRIBUTING.md#use-of-generative-ai), we request that you don't use LLMs to generate PRs as those are not a good use of maintainer time." \ - -f query=' - mutation CommentAndClosePR($prId: ID!, $body: String!) { - addComment(input: { - subjectId: $prId, - body: $body - }) { - __typename - } - - closePullRequest(input: { - pullRequestId: $prId - }) { - __typename - } - }' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3450fe96bb..2378b032b6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -44,7 +44,7 @@ jobs: steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -57,7 +57,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 + uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -70,7 +70,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@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 + uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 # â„šī¸ 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 @@ -83,6 +83,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 + uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2573ba8123..84509103be 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -23,14 +23,14 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 + uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -60,7 +60,7 @@ jobs: suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn'] steps: - name: Login to GitHub Container Registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -90,7 +90,7 @@ jobs: suffix: [''] steps: - name: Login to GitHub Container Registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -132,7 +132,7 @@ jobs: suffixes: '-rocm' platforms: linux/amd64 runner-mapping: '{"linux/amd64": "pokedex-large"}' - uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@bd49ed7a5a6022149f79b6564df48177476a822b # multi-runner-build-workflow-v2.2.1 + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0 permissions: contents: read actions: read @@ -155,7 +155,7 @@ jobs: name: Build and Push Server needs: pre-job if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }} - uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@bd49ed7a5a6022149f79b6564df48177476a822b # multi-runner-build-workflow-v2.2.1 + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0 permissions: contents: read actions: read @@ -178,7 +178,7 @@ jobs: runs-on: ubuntu-latest if: always() steps: - - uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4 + - uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5 with: needs: ${{ toJSON(needs) }} @@ -189,6 +189,6 @@ jobs: runs-on: ubuntu-latest if: always() steps: - - uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4 + - uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5 with: needs: ${{ toJSON(needs) }} diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 02d7b3456a..0ccebfb363 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -21,14 +21,14 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 + uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -54,7 +54,7 @@ jobs: steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -67,7 +67,7 @@ jobs: fetch-depth: 0 - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -86,7 +86,7 @@ jobs: run: pnpm build - name: Upload build output - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docs-build-output path: docs/build/ diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index babda72c33..8aa063e1bb 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -20,7 +20,7 @@ jobs: artifact: ${{ steps.get-artifact.outputs.result }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -29,7 +29,7 @@ jobs: run: echo 'The triggering workflow did not succeed' && exit 1 - name: Get artifact id: get-artifact - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ steps.token.outputs.token }} script: | @@ -48,7 +48,7 @@ jobs: return { found: true, id: matchArtifact.id }; - name: Determine deploy parameters id: parameters - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: HEAD_SHA: ${{ github.event.workflow_run.head_sha }} with: @@ -119,7 +119,7 @@ jobs: if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -131,11 +131,11 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@dab18118da6476e8237ac94080fd937983fecd42 # use-mise-action-v1.1.2 + uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3 - name: Load parameters id: parameters - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: PARAM_JSON: ${{ needs.checks.outputs.parameters }} with: @@ -147,7 +147,7 @@ jobs: core.setOutput("shouldDeploy", parameters.shouldDeploy); - name: Download artifact - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }} with: diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml index 05842889cc..bb24a017fe 100644 --- a/.github/workflows/docs-destroy.yml +++ b/.github/workflows/docs-destroy.yml @@ -17,7 +17,7 @@ jobs: pull-requests: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -29,7 +29,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@dab18118da6476e8237ac94080fd937983fecd42 # use-mise-action-v1.1.2 + uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3 - name: Destroy Docs Subdomain env: diff --git a/.github/workflows/fix-format.yml b/.github/workflows/fix-format.yml index 0091bcef89..59cbb28fa8 100644 --- a/.github/workflows/fix-format.yml +++ b/.github/workflows/fix-format.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -29,7 +29,7 @@ jobs: persist-credentials: true - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb # v6.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -42,13 +42,13 @@ jobs: run: pnpm --recursive install && pnpm run --recursive --if-present --parallel format:fix - name: Commit and push - uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4 + uses: EndBug/add-and-commit@290ea2c423ad77ca9c62ae0f5b224379612c0321 # v10.0.0 with: default_author: github_actions message: 'chore: fix formatting' - name: Remove label - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 if: always() with: github-token: ${{ steps.generate-token.outputs.token }} diff --git a/.github/workflows/merge-translations.yml b/.github/workflows/merge-translations.yml index 392dec5e37..08d3192f8b 100644 --- a/.github/workflows/merge-translations.yml +++ b/.github/workflows/merge-translations.yml @@ -31,7 +31,7 @@ jobs: - name: Generate a token id: generate_token if: ${{ inputs.skip != true }} - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml index e04b32d74f..416e40df0d 100644 --- a/.github/workflows/pr-label-validation.yml +++ b/.github/workflows/pr-label-validation.yml @@ -14,13 +14,13 @@ jobs: pull-requests: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Require PR to have a changelog label - uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5.5.1 + uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5.5.2 with: token: ${{ steps.token.outputs.token }} mode: exactly diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 24f3f8faf1..75ee750e9f 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 6030dc8752..5731c06372 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -50,7 +50,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -63,10 +63,10 @@ jobs: ref: main - name: Install uv - uses: astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a # v7.4.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb # v6.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -86,7 +86,7 @@ jobs: - name: Commit and tag id: push-tag - uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4 + uses: EndBug/add-and-commit@290ea2c423ad77ca9c62ae0f5b224379612c0321 # v10.0.0 with: default_author: github_actions message: 'chore: version ${{ steps.output.outputs.version }}' @@ -124,7 +124,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -136,13 +136,13 @@ jobs: persist-credentials: false - name: Download APK - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: release-apk-signed github-token: ${{ steps.generate-token.outputs.token }} - name: Create draft release - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 with: draft: true tag_name: ${{ needs.bump_version.outputs.version }} @@ -151,6 +151,7 @@ jobs: body_path: misc/release/notes.tmpl files: | docker/docker-compose.yml + docker/docker-compose.rootless.yml docker/example.env docker/hwaccel.ml.yml docker/hwaccel.transcoding.yml diff --git a/.github/workflows/preview-label.yaml b/.github/workflows/preview-label.yaml index dc6f0eff0a..5cf0008597 100644 --- a/.github/workflows/preview-label.yaml +++ b/.github/workflows/preview-label.yaml @@ -14,12 +14,12 @@ jobs: pull-requests: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2 + - uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0 with: github-token: ${{ steps.token.outputs.token }} message-id: 'preview-status' @@ -32,12 +32,12 @@ jobs: pull-requests: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ steps.token.outputs.token }} script: | @@ -48,14 +48,14 @@ jobs: name: 'preview' }) - - uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2 + - uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0 if: ${{ github.event.pull_request.head.repo.fork }} with: github-token: ${{ steps.token.outputs.token }} message-id: 'preview-status' message: 'PRs from forks cannot have preview environments.' - - uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2 + - uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0 if: ${{ !github.event.pull_request.head.repo.fork }} with: github-token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index 2da7d79b26..d9b6ffb7f5 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -19,7 +19,7 @@ jobs: working-directory: ./open-api/typescript-sdk steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -30,7 +30,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 # Setup .npmrc file to publish to npm - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index d100dd281f..21e5e25bc6 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -20,14 +20,14 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 + uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -49,7 +49,7 @@ jobs: working-directory: ./mobile steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -61,7 +61,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Flutter SDK - uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0 + uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 with: channel: 'stable' flutter-version-file: ./mobile/pubspec.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2a2ebe2389..4558b90866 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,14 +17,14 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 + uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -63,7 +63,7 @@ jobs: working-directory: ./server steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -75,7 +75,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: @@ -108,7 +108,7 @@ jobs: working-directory: ./cli steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -119,7 +119,7 @@ jobs: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: @@ -155,7 +155,7 @@ jobs: working-directory: ./cli steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -166,7 +166,7 @@ jobs: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: @@ -197,7 +197,7 @@ jobs: working-directory: ./web steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -208,7 +208,7 @@ jobs: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: @@ -241,7 +241,7 @@ jobs: working-directory: ./web steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -252,7 +252,7 @@ jobs: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: @@ -279,7 +279,7 @@ jobs: contents: read steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -290,7 +290,7 @@ jobs: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: @@ -327,7 +327,7 @@ jobs: working-directory: ./e2e steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -338,7 +338,7 @@ jobs: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: @@ -373,7 +373,7 @@ jobs: working-directory: ./server steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -385,7 +385,7 @@ jobs: submodules: 'recursive' token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: @@ -412,7 +412,7 @@ jobs: runner: [ubuntu-latest, ubuntu-24.04-arm] steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -424,7 +424,7 @@ jobs: submodules: 'recursive' token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: @@ -464,7 +464,7 @@ jobs: run: docker compose logs --no-color > docker-compose-logs.txt working-directory: ./e2e - name: Archive Docker logs - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: always() with: name: e2e-server-docker-logs-${{ matrix.runner }} @@ -484,7 +484,7 @@ jobs: runner: [ubuntu-latest, ubuntu-24.04-arm] steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -496,7 +496,7 @@ jobs: submodules: 'recursive' token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: @@ -522,7 +522,7 @@ jobs: run: pnpm test:web if: ${{ !cancelled() }} - name: Archive e2e test (web) results - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: success() || failure() with: name: e2e-web-test-results-${{ matrix.runner }} @@ -533,7 +533,7 @@ jobs: run: pnpm test:web:ui if: ${{ !cancelled() }} - name: Archive ui test (web) results - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: success() || failure() with: name: e2e-ui-test-results-${{ matrix.runner }} @@ -544,7 +544,7 @@ jobs: run: pnpm test:web:maintenance if: ${{ !cancelled() }} - name: Archive maintenance tests (web) results - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: success() || failure() with: name: e2e-maintenance-isolated-test-results-${{ matrix.runner }} @@ -554,7 +554,7 @@ jobs: run: docker compose logs --no-color > docker-compose-logs.txt working-directory: ./e2e - name: Archive Docker logs - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: always() with: name: e2e-web-docker-logs-${{ matrix.runner }} @@ -566,7 +566,7 @@ jobs: runs-on: ubuntu-latest if: always() steps: - - uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4 + - uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5 with: needs: ${{ toJSON(needs) }} mobile-unit-tests: @@ -578,7 +578,7 @@ jobs: contents: read steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -588,7 +588,7 @@ jobs: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup Flutter SDK - uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0 + uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 with: channel: 'stable' flutter-version-file: ./mobile/pubspec.yaml @@ -610,7 +610,7 @@ jobs: working-directory: ./machine-learning steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -620,7 +620,7 @@ jobs: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Install uv - uses: astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a # v7.4.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: python-version: 3.11 - name: Install dependencies @@ -650,7 +650,7 @@ jobs: working-directory: ./.github steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -661,7 +661,7 @@ jobs: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: @@ -680,7 +680,7 @@ jobs: contents: read steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -701,7 +701,7 @@ jobs: contents: read steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -712,7 +712,7 @@ jobs: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: @@ -763,7 +763,7 @@ jobs: working-directory: ./server steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -774,7 +774,7 @@ jobs: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index 6e997ad76a..09024063c0 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -24,14 +24,14 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 + uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -47,7 +47,7 @@ jobs: if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 + uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -68,6 +68,6 @@ jobs: permissions: {} if: always() steps: - - uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4 + - uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5 with: needs: ${{ toJSON(needs) }} diff --git a/.gitignore b/.gitignore index 3220701cc6..e8fdfa266c 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ vite.config.js.timestamp-* .pnpm-store .devcontainer/library .devcontainer/.env* +*.tsbuildinfo +*.tsbuildInfo diff --git a/.gitmodules b/.gitmodules index d417dc5ba8..50a43933a9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "mobile/.isar"] - path = mobile/.isar - url = https://github.com/isar/isar [submodule "e2e/test-assets"] path = e2e/test-assets url = https://github.com/immich-app/test-assets diff --git a/cli/.nvmrc b/cli/.nvmrc index 32f8c50de0..8e35034890 100644 --- a/cli/.nvmrc +++ b/cli/.nvmrc @@ -1 +1 @@ -24.13.1 +24.14.1 diff --git a/cli/package.json b/cli/package.json index e5e7400efd..108e65f945 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.6.0", + "version": "2.7.5", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", @@ -20,7 +20,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^24.11.0", + "@types/node": "^24.12.2", "@vitest/coverage-v8": "^4.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -28,15 +28,14 @@ "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^63.0.0", + "eslint-plugin-unicorn": "^64.0.0", "globals": "^17.0.0", "mock-fs": "^5.2.0", "prettier": "^3.7.4", "prettier-plugin-organize-imports": "^4.0.0", - "typescript": "^5.3.3", - "typescript-eslint": "^8.28.0", - "vite": "^7.0.0", - "vite-tsconfig-paths": "^6.0.0", + "typescript": "^6.0.0", + "typescript-eslint": "^8.58.0", + "vite": "^8.0.0", "vitest": "^4.0.0", "vitest-fetch-mock": "^0.4.0", "yaml": "^2.3.1" @@ -69,6 +68,6 @@ "micromatch": "^4.0.8" }, "volta": { - "node": "24.13.1" + "node": "24.14.1" } } diff --git a/cli/src/commands/asset.spec.ts b/cli/src/commands/asset.spec.ts index 21700ef963..f179b350c9 100644 --- a/cli/src/commands/asset.spec.ts +++ b/cli/src/commands/asset.spec.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { setTimeout as sleep } from 'node:timers/promises'; import { describe, expect, it, MockedFunction, vi } from 'vitest'; -import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk'; +import { AssetRejectReason, AssetUploadAction, checkBulkUpload, defaults, getSupportedMediaTypes } from '@immich/sdk'; import createFetchMock from 'vitest-fetch-mock'; import { @@ -120,7 +120,7 @@ describe('checkForDuplicates', () => { vi.mocked(checkBulkUpload).mockResolvedValue({ results: [ { - action: Action.Accept, + action: AssetUploadAction.Accept, id: testFilePath, }, ], @@ -144,10 +144,10 @@ describe('checkForDuplicates', () => { vi.mocked(checkBulkUpload).mockResolvedValue({ results: [ { - action: Action.Reject, + action: AssetUploadAction.Reject, id: testFilePath, assetId: 'fc5621b1-86f6-44a1-9905-403e607df9f5', - reason: Reason.Duplicate, + reason: AssetRejectReason.Duplicate, }, ], }); @@ -167,7 +167,7 @@ describe('checkForDuplicates', () => { vi.mocked(checkBulkUpload).mockResolvedValue({ results: [ { - action: Action.Accept, + action: AssetUploadAction.Accept, id: testFilePath, }, ], @@ -187,7 +187,7 @@ describe('checkForDuplicates', () => { mocked.mockResolvedValue({ results: [ { - action: Action.Accept, + action: AssetUploadAction.Accept, id: testFilePath, }, ], diff --git a/cli/src/commands/asset.ts b/cli/src/commands/asset.ts index 7d4b09b69d..2c6430c83a 100644 --- a/cli/src/commands/asset.ts +++ b/cli/src/commands/asset.ts @@ -1,9 +1,9 @@ import { - Action, AssetBulkUploadCheckItem, AssetBulkUploadCheckResult, AssetMediaResponseDto, AssetMediaStatus, + AssetUploadAction, Permission, addAssetsToAlbum, checkBulkUpload, @@ -234,7 +234,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas const results = response.results as AssetBulkUploadCheckResults; for (const { id: filepath, assetId, action } of results) { - if (action === Action.Accept) { + if (action === AssetUploadAction.Accept) { newFiles.push(filepath); } else { // rejects are always duplicates @@ -404,8 +404,6 @@ const uploadFile = async (input: string, stats: Stats): Promise Settings). @@ -62,6 +67,8 @@ Once you have a new OAuth client application configured, Immich can be configure | `scope` | string | openid email profile | Full list of scopes to send with the request (space delimited) | | `id_token_signed_response_alg` | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) | | `userinfo_signed_response_alg` | string | none | The algorithm used to sign the userinfo response (examples: RS256, HS256) | +| `prompt` | string | (empty) | Prompt parameter for authorization url (examples: select_account, login, consent) | +| `end_session_endpoint` | URL | (empty) | Http(s) alternative end session endpoint (logout URI) | | Request timeout | string | 30,000 (30 seconds) | Number of milliseconds to wait for http requests to complete before giving up | | Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**š** | | Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**š** | @@ -180,6 +187,7 @@ Configuration of OAuth in Immich System Settings | Scope | openid email profile immich_scope | | ID Token Signed Response Algorithm | RS256 | | Userinfo Signed Response Algorithm | RS256 | +| End Session Endpoint | https://auth.example.com/logout?rd=https://immich.example.com/ | | Storage Label Claim | uid | | Storage Quota Claim | immich_quota | | Default Storage Quota (GiB) | 0 (empty for unlimited quota) | @@ -253,4 +261,40 @@ Configuration of OAuth in Immich System Settings +
+Keycloak Example + +### Keycloak Example + +Here's an example of OAuth configured for Keycloak: + +Create your immich client on your Keycloak Realm. + + + + + +Configuration of OAuth in Immich System Settings + +| Setting | Value | +| ---------------------------- | ----------------------------------------------------- | +| Issuer URL | `https:///realms/` | +| Client ID | immich | +| Client Secret | can be optained from Clients -> immich -> Credentials | +| Scope | openid email profile | +| Signing Algorithm | RS256 | +| Storage Label Claim | preferred_username | +| Role Claim | immich_role | +| Storage Quota Claim | immich_quota | +| Default Storage Quota (GiB) | 0 (empty for unlimited quota) | +| Button Text | Sign in with Keycloak (recommended) | +| Auto Register | Enabled (optional) | +| Auto Launch | Enabled (optional) | +| Mobile Redirect URI Override | Disabled | +| Mobile Redirect URI | | + +Role Claim can be managed via Client Role. Remember to create a mapper with claim name `immich_role`. + +
+ [oidc]: https://openid.net/connect/ diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md index 4bbf71dd89..abdb3befbe 100644 --- a/docs/docs/developer/setup.md +++ b/docs/docs/developer/setup.md @@ -80,9 +80,9 @@ To see local changes to `@immich/ui` in Immich, do the following: 1. Install `@immich/ui` as a sibling to `immich/`, for example `/home/user/immich` and `/home/user/ui` 2. Build the `@immich/ui` project via `pnpm run build` -3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`) -4. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`) -5. Uncomment the import statement in `web/src/app.css` file `@import '/usr/ui/dist/theme/default.css';` and comment out `@import '@immich/ui/theme/default.css';` +3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yml` file (`../../ui:/usr/src/ui`) +4. Uncomment the corresponding alias in the `web/vite.config.ts` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui/packages/ui')`) +5. Uncomment the import statement in `web/src/app.css` file `@import '../../../ui/packages/ui/dist/theme/default.css';` and comment out `@import '@immich/ui/theme/default.css';` 6. Start up the stack via `make dev` 7. After making changes in `@immich/ui`, rebuild it (`pnpm run build`) diff --git a/docs/docs/features/duplicates-utility.md b/docs/docs/features/duplicates-utility.md new file mode 100644 index 0000000000..f790c42708 --- /dev/null +++ b/docs/docs/features/duplicates-utility.md @@ -0,0 +1,28 @@ +# Duplicates Utility + +Immich comes with a duplicates utility to help you detect assets that look visually similar. The duplicate detection feature relies on machine learning and is enabled by default. For more information about when the duplicate detection job runs, see [Jobs and Workers](/administration/jobs-workers). Once an asset has been processed and added to a duplicate group, it becomes available to review in the "Review duplicates" utility, which can be found [here](https://my.immich.app/utilities/duplicates). + +## Reviewing duplicates + +The review duplicates page allows the user to individually select which assets should be kept and which ones should be trashed. When more than one asset is kept, there is an option to automatically put the kept assets into a stack. + +### Automatic preselection + +When using "Deduplicate All" or viewing suggestions, Immich automatically preselects which assets to keep based on: + +1. **Image size in bytes** — larger files are preferred as they typically have higher quality. +2. **Count of EXIF data** — assets with more metadata are preferred. + +### Synchronizing metadata + +When resolving duplicates, metadata from trashed assets is automatically synchronized to the kept assets. The following metadata is synchronized: + +| Name | Description | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------- | +| Album | The kept assets will be added to _every_ album that the other assets in the group belong to. | +| Favorite | If any of the assets in the group have been added to favorites, every kept asset will also be added to favorites. | +| Rating | If one or more assets in the duplicate group have a rating, the highest rating is selected and synchronized to the kept assets. | +| Description | Descriptions from each asset are combined together and synchronized to all the kept assets. | +| Visibility | The most restrictive visibility is applied to the kept assets. | +| Location | Latitude and longitude are copied if all assets with geolocation data in the group share the same coordinates. | +| Tag | Tags from all assets in the group are merged and applied to every kept asset. | diff --git a/docs/docs/features/searching.md b/docs/docs/features/searching.md index 7360787127..92eb01c39d 100644 --- a/docs/docs/features/searching.md +++ b/docs/docs/features/searching.md @@ -26,7 +26,7 @@ You can search the following types of content: | Time frame | Start and end date of a specific time bucket | | Media type | Image or video or both | | Display options | In Archive, in Favorites or Not in any album | -| Start rating | User-assigned start rating | +| Star rating | User-assigned star rating | diff --git a/docs/docs/features/supported-formats.md b/docs/docs/features/supported-formats.md index 4c4ac6039a..86ac264cc3 100644 --- a/docs/docs/features/supported-formats.md +++ b/docs/docs/features/supported-formats.md @@ -28,17 +28,17 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a ## Video formats -| Format | Extension(s) | Supported? | Notes | -| :---------- | :-------------------- | :----------------: | :---- | -| `3GPP` | `.3gp` `.3gpp` | :white_check_mark: | | -| `AVI` | `.avi` | :white_check_mark: | | -| `FLV` | `.flv` | :white_check_mark: | | -| `M4V` | `.m4v` | :white_check_mark: | | -| `MATROSKA` | `.mkv` | :white_check_mark: | | -| `MP2T` | `.mts` `.m2ts` `.m2t` | :white_check_mark: | | -| `MP4` | `.mp4` `.insv` | :white_check_mark: | | -| `MPEG` | `.mpg` `.mpe` `.mpeg` | :white_check_mark: | | -| `MXF` | `.mxf` | :white_check_mark: | | -| `QUICKTIME` | `.mov` | :white_check_mark: | | -| `WEBM` | `.webm` | :white_check_mark: | | -| `WMV` | `.wmv` | :white_check_mark: | | +| Format | Extension(s) | Supported? | Notes | +| :---------- | :-------------------------- | :----------------: | :---- | +| `3GPP` | `.3gp` `.3gpp` | :white_check_mark: | | +| `AVI` | `.avi` | :white_check_mark: | | +| `FLV` | `.flv` | :white_check_mark: | | +| `M4V` | `.m4v` | :white_check_mark: | | +| `MATROSKA` | `.mkv` | :white_check_mark: | | +| `MP2T` | `.mts` `.m2ts` `.m2t` `.ts` | :white_check_mark: | | +| `MP4` | `.mp4` `.insv` | :white_check_mark: | | +| `MPEG` | `.mpg` `.mpe` `.mpeg` | :white_check_mark: | | +| `MXF` | `.mxf` | :white_check_mark: | | +| `QUICKTIME` | `.mov` | :white_check_mark: | | +| `WEBM` | `.webm` | :white_check_mark: | | +| `WMV` | `.wmv` | :white_check_mark: | | diff --git a/docs/docs/guides/custom-map-styles.md b/docs/docs/guides/custom-map-styles.md index 1a61afc324..ac693c16ba 100644 --- a/docs/docs/guides/custom-map-styles.md +++ b/docs/docs/guides/custom-map-styles.md @@ -3,8 +3,8 @@ You may decide that you'd like to modify the style document which is used to draw the maps in Immich. In addition to visual customization, this also allows you to pick your own map tile provider instead of the default one. The default -`style.json` for [light theme](https://github.com/immich-app/immich/tree/main/server/resources/style-light.json) -and [dark theme](https://github.com/immich-app/immich/blob/main/server/resources/style-dark.json) +`style.json` for [light theme](https://tiles.immich.cloud/v1/style/light.json) +and [dark theme](https://tiles.immich.cloud/v1/style/dark.json) can be used as a basis for creating your own style. There are several sources for already-made `style.json` map themes, as well as diff --git a/docs/docs/guides/python-file-upload.md b/docs/docs/guides/python-file-upload.md index 684524f9c4..6816924e6f 100644 --- a/docs/docs/guides/python-file-upload.md +++ b/docs/docs/guides/python-file-upload.md @@ -20,8 +20,6 @@ def upload(file): } data = { - 'deviceAssetId': f'{file}-{stats.st_mtime}', - 'deviceId': 'python', 'fileCreatedAt': datetime.fromtimestamp(stats.st_mtime), 'fileModifiedAt': datetime.fromtimestamp(stats.st_mtime), 'isFavorite': 'false', diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index 3355750603..4754497d90 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -193,6 +193,7 @@ The default configuration looks like this: "defaultStorageQuota": null, "enabled": false, "issuerUrl": "", + "endSessionEndpoint": "", "mobileOverrideEnabled": false, "mobileRedirectUri": "", "profileSigningAlgorithm": "none", diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index e9e3bb032c..b29c233153 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -29,22 +29,23 @@ These environment variables are used by the `docker-compose.yml` file and do **N ## General -| Variable | Description | Default | Containers | Workers | -| :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- | -| `TZ` | Timezone | \*1 | server | microservices | -| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices | -| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | -| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices | -| `IMMICH_MEDIA_LOCATION` | Media location inside the container âš ī¸**You probably shouldn't set this**\*2âš ī¸ | `/data` | server | api, microservices | -| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | -| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | | -| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | | -| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api | -| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices | -| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices | -| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api | -| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices | -| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api | +| Variable | Description | Default | Containers | Workers | +| :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- | +| `TZ` | Timezone | \*1 | server | microservices | +| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices | +| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | +| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices | +| `IMMICH_MEDIA_LOCATION` | Media location inside the container âš ī¸**You probably shouldn't set this**\*2âš ī¸ | `/data` | server | api, microservices | +| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | +| `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`. | `false` | server | api | +| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | | +| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | | +| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api | +| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices | +| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices | +| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api | +| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices | +| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api | \*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`. `TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution. diff --git a/docs/docs/install/requirements.md b/docs/docs/install/requirements.md index ee5db45c9a..66f3033a43 100644 --- a/docs/docs/install/requirements.md +++ b/docs/docs/install/requirements.md @@ -8,7 +8,7 @@ Hardware and software requirements for Immich: ## Hardware -- **OS**: Recommended Linux or \*nix operating system (Ubuntu, Debian, etc). +- **OS**: Recommended Linux or \*nix 64-bit operating system (Ubuntu, Debian, etc). - Non-Linux OSes tend to provide a poor Docker experience and are strongly discouraged. Our ability to assist with setup or troubleshooting on non-Linux OSes will be severely reduced. If you still want to try to use a non-Linux OS, you can set it up as follows: @@ -19,6 +19,10 @@ Hardware and software requirements for Immich: If you have issues, we recommend that you switch to a supported VM deployment. - **RAM**: Minimum 6GB, recommended 8GB. - **CPU**: Minimum 2 cores, recommended 4 cores. + - Immich runs on the `amd64` and `arm64` platforms. + Since `v2.6`, the machine learning container on `amd64` requires the `>= x86-64-v2` [microarchitecture level](https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels). + Most CPUs released since ~2012 support this microarchitecture. + If you are using a virtual machine, ensure you have selected a [supported microarchitecture](https://pve.proxmox.com/pve-docs/chapter-qm.html#_qemu_cpu_types). - **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions. - The generation of thumbnails and transcoded video can increase the size of the photo library by 10-20% on average. @@ -45,7 +49,7 @@ Immich requires [**Docker**](https://docs.docker.com/get-started/get-docker/) wi The Compose plugin will be installed by both Docker Engine and Desktop by following the linked installation guides; it can also be [separately installed](https://docs.docker.com/compose/install/). :::note -Immich requires the command `docker compose`; the similarly named `docker-compose` is [deprecated](https://docs.docker.com/compose/migrate/) and is no longer supported by Immich. +Immich requires the command `docker compose`; the similarly named `docker-compose` is [deprecated](https://docs.docker.com/retired/#docker-compose-v1-replaced-by-compose-v2) and is no longer supported by Immich. ::: ### Special requirements for Windows users diff --git a/docs/docs/partials/_storage-template.md b/docs/docs/partials/_storage-template.md index 84236e0ac1..1cd9572c11 100644 --- a/docs/docs/partials/_storage-template.md +++ b/docs/docs/partials/_storage-template.md @@ -6,6 +6,8 @@ You can read more about the differences between storage template engine on and o The admin user can set the template by using the template builder in the `Administration -> Settings -> Storage Template`. Immich provides a set of variables that you can use in constructing the template, along with additional custom text. If the template produces [multiple files with the same filename, they won't be overwritten](https://github.com/immich-app/immich/discussions/3324) as a sequence number is appended to the filename. +Date and time variables in storage templates are rendered in the server's local timezone. + ```bash title="Default template" Year/Year-Month-Day/Filename.Extension ``` diff --git a/docs/package.json b/docs/package.json index 60a6dccf87..f976791279 100644 --- a/docs/package.json +++ b/docs/package.json @@ -30,17 +30,17 @@ "postcss": "^8.4.25", "prism-react-renderer": "^2.3.1", "raw-loader": "^4.0.2", - "react": "^18.0.0", - "react-dom": "^18.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", "tailwindcss": "^3.2.4", "url": "^0.11.0" }, "devDependencies": { "@docusaurus/module-type-aliases": "~3.9.0", - "@docusaurus/tsconfig": "^3.7.0", + "@docusaurus/tsconfig": "^3.10.0", "@docusaurus/types": "^3.7.0", "prettier": "^3.7.4", - "typescript": "^5.1.6" + "typescript": "^6.0.0" }, "browserslist": { "production": [ @@ -58,6 +58,6 @@ "node": ">=20" }, "volta": { - "node": "24.13.1" + "node": "24.14.1" } } diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 83206fefee..964291ad08 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,7 +1,11 @@ [ { - "label": "v2.6.0", - "url": "https://docs.v2.6.0.archive.immich.app" + "label": "v2.7.5", + "url": "https://docs.v2.7.5.archive.immich.app" + }, + { + "label": "v2.6.3", + "url": "https://docs.v2.6.3.archive.immich.app" }, { "label": "v2.5.6", diff --git a/docs/tsconfig.json b/docs/tsconfig.json index 674c46e46d..a6ba1bd9dd 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -1,8 +1,4 @@ { // This file is not used in compilation. It is here just for a nice editor experience. - "extends": "@docusaurus/tsconfig", - - "compilerOptions": { - "baseUrl": "." - } + "extends": "@docusaurus/tsconfig" } diff --git a/e2e-auth-server/auth-server.ts b/e2e-auth-server/auth-server.ts index 9aef56510d..15aaa71c1c 100644 --- a/e2e-auth-server/auth-server.ts +++ b/e2e-auth-server/auth-server.ts @@ -1,5 +1,12 @@ -import { exportJWK, generateKeyPair } from 'jose'; +import { + calculateJwkThumbprint, + exportJWK, + importPKCS8, + importSPKI, + SignJWT, +} from 'jose'; import Provider from 'oidc-provider'; +import { PRIVATE_KEY_PEM, PUBLIC_KEY_PEM } from './test-keys'; export enum OAuthClient { DEFAULT = 'client-default', @@ -44,6 +51,29 @@ const claims = [ }, ]; +const privateKey = await importPKCS8(PRIVATE_KEY_PEM, 'RS256', { + extractable: true, +}); +const publicKey = await importSPKI(PUBLIC_KEY_PEM, 'RS256', { + extractable: true, +}); +const kid = await calculateJwkThumbprint(await exportJWK(publicKey)); + +export async function generateLogoutToken(iss: string, sub: string) { + return await new SignJWT({ + iss: iss, + aud: OAuthClient.DEFAULT, + iat: Math.floor(Date.now() / 1000), + jti: crypto.randomUUID(), + sub: sub, + events: { + 'http://schemas.openid.net/event/backchannel-logout': {}, + }, + }) + .setProtectedHeader({ alg: 'RS256', typ: 'logout+jwt', kid: kid }) + .sign(privateKey); +} + const withDefaultClaims = (sub: string) => ({ sub, email: `${sub}@immich.app`, @@ -66,8 +96,6 @@ const getClaims = (sub: string, use?: string) => { }; const setup = async () => { - const { privateKey, publicKey } = await generateKeyPair('RS256'); - const redirectUris = [ 'http://127.0.0.1:2285/auth/login', 'https://photos.immich.app/oauth/mobile-redirect', diff --git a/e2e-auth-server/package.json b/e2e-auth-server/package.json index 73ede1b7c4..f8ea7243fd 100644 --- a/e2e-auth-server/package.json +++ b/e2e-auth-server/package.json @@ -7,7 +7,7 @@ "start": "tsx startup.ts" }, "devDependencies": { - "jose": "^5.6.3", + "jose": "^6.0.0", "@types/oidc-provider": "^9.0.0", "oidc-provider": "^9.0.0", "tsx": "^4.20.6" diff --git a/e2e-auth-server/test-keys.ts b/e2e-auth-server/test-keys.ts new file mode 100644 index 0000000000..a37e822029 --- /dev/null +++ b/e2e-auth-server/test-keys.ts @@ -0,0 +1,38 @@ +export const PRIVATE_KEY_PEM = `-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCVj5C7hzN3E2HO +TcJ+DN/e2NSTQFj4rPylz4J8xjm8Es7l0k2kK5EEGvUNVGZbw7s055c+6kwP9eqg +B5XFE7+26Fcq1sou6Tbm310kU4dnMW5l2CgwrhaGyb1pNysao0AMLT60dFYqtUwn +ha9ceCsa+ZU1JrknVf3rONtppBvhWoI7CO9XX1keVQ0unHPzCWUjpXTzC8OGEbmB +2w7ZIUf8OfJkd5RZ4OtIpML71W9n13aDxT50x2/EW/pFLFtQ/oaleOKHpvlRXDRX +W86G4moUJym3gHMXMUj2aOcFG2UJnpLruKz3i5qZwYiTRlBP6O9EIQNCVtYxchuN +V1CCcBU1AgMBAAECggEAJLfXMu8Nx89ynPVyyUMMaFfoEpHC9iR0L5obQVpiPMYK +VRqVVLecdftPS9s7eQ58BNBRzdC0ZVu841aRYs3HLNbsZZhPkYZQpAxU//Dg5okY +fzj7Hv5yidt4HN9+Pd8z/3lRMnj4WapifLaBt8xJ2ujJBMBRxzJBsXDnT0+Kx7+y +bYDeuVfyUTEikaK3QZTbuRF3D3eiuN16GG+hv8UqTF2eYbPxdiLjYpTSHa4mH88C +qfJz2Xt4SEzmyeo3G+MO17wDFOwtEe8ojlJfULHnHJSFdUwTfYIFM1bg5/fJ9MOS +/fO3TSG+wkQqjQa6eoGssAzP87fL2XNLzlDtGY/7uQKBgQDHuJHOtf1EjOvNYiP7 +EN+8QGs41ghzt9CQRQxWbHpusR3IW3P83KMXwYmrlG70oOUXBRGSB/ESXUofXc5W +pu5+Y55S44aUnu/a9yOBttYW0dtHZSL0zFT+PlVASwUzFZ2zcH1KXlUkSpfL5OAD +PyDDTnBZ2AWh45fRO9wLo6PPuQKBgQC/tI03RqU3mOjqukKbquYeIpXHfRU5Z0DM +u9ru1THYEl6fmkMXycxo/mvW3awyFuyKy/VodqIgKnFgumEqCHZh6OAMm/LC7TfA +l9tjFSs/MyOqQVD4kbX+z6Oq4c4GccDoXfsQ3gzECoBapegi/F+6/25y+/C8ghXb +J/Jg1GQXXQKBgQDFgWbfzuVZZyrBfu4qGLPJDMN7/114YizknwPma3xf/tN/EcGQ +K/k1QvWMMkvPq1UiAKcxjJ0AFjV482FcG9T6NDWbrtmmG88C8Sex3Ue2ZW2+GuwI +vhDHJIlV/Vp0/Elp7DJa2xLDwuh+gCZvz3vs6KL+ljxrrhCyn8mp0PfsMQKBgFFZ +KnuETOO0zVGdzFoGQTQUdP58A5+iQwsdxB+I9Ge+E80iRso3ZbhADj7VPhbbR3D2 +b6LuhImluQrUzBpsEOAnU7vGCVPSGdBuIDiBaSKebsn2gYeZPWNtdQQ0YZq2dqek +Cb/0mfIuipzsvf7qnSza62F7q4IyqVegMegI+Jg5AoGATM3NMy7JZeKzSkm+3ohU +3xZOwgqKV9SH+0OeYWpuBxT7D7FlrKKI4NJ3XN3hg2f/DJAF6dH11CPe7pk94yol +HMbh+PQUQ6GYvAzxIOvagWboQ3lzeyubNMpyFjfOrIE/WOQCUBZ9tIwCHIarIuyi +QRuNOj3+U8T/n1Ww352HBdw= +-----END PRIVATE KEY-----`; + +export const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlY+Qu4czdxNhzk3Cfgzf +3tjUk0BY+Kz8pc+CfMY5vBLO5dJNpCuRBBr1DVRmW8O7NOeXPupMD/XqoAeVxRO/ +tuhXKtbKLuk25t9dJFOHZzFuZdgoMK4Whsm9aTcrGqNADC0+tHRWKrVMJ4WvXHgr +GvmVNSa5J1X96zjbaaQb4VqCOwjvV19ZHlUNLpxz8wllI6V08wvDhhG5gdsO2SFH +/DnyZHeUWeDrSKTC+9VvZ9d2g8U+dMdvxFv6RSxbUP6GpXjih6b5UVw0V1vOhuJq +FCcpt4BzFzFI9mjnBRtlCZ6S67is94uamcGIk0ZQT+jvRCEDQlbWMXIbjVdQgnAV +NQIDAQAB +-----END PUBLIC KEY-----`; diff --git a/e2e/.nvmrc b/e2e/.nvmrc index 32f8c50de0..8e35034890 100644 --- a/e2e/.nvmrc +++ b/e2e/.nvmrc @@ -1 +1 @@ -24.13.1 +24.14.1 diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index 957de4698e..c8a3b975d4 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -44,7 +44,7 @@ services: redis: container_name: immich-e2e-redis - image: docker.io/valkey/valkey:9@sha256:3eeb09785cd61ec8e3be35f8804c8892080f3ca21934d628abc24ee4ed1698f6 + image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9 healthcheck: test: redis-cli ping || exit 1 diff --git a/e2e/package.json b/e2e/package.json index 1220d91418..6b72c1b36d 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "2.6.0", + "version": "2.7.5", "description": "", "main": "index.js", "type": "module", @@ -32,15 +32,15 @@ "@playwright/test": "^1.44.1", "@socket.io/component-emitter": "^3.1.2", "@types/luxon": "^3.4.2", - "@types/node": "^24.11.0", + "@types/node": "^24.12.2", "@types/pg": "^8.15.1", "@types/pngjs": "^6.0.4", - "@types/supertest": "^6.0.2", + "@types/supertest": "^7.0.0", "dotenv": "^17.2.3", "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^63.0.0", + "eslint-plugin-unicorn": "^64.0.0", "exiftool-vendored": "^35.0.0", "globals": "^17.0.0", "luxon": "^3.4.4", @@ -51,13 +51,13 @@ "sharp": "^0.34.5", "socket.io-client": "^4.7.4", "supertest": "^7.0.0", - "typescript": "^5.3.3", + "typescript": "^6.0.0", "typescript-eslint": "^8.28.0", "utimes": "^5.2.1", "vite-tsconfig-paths": "^6.1.1", "vitest": "^4.0.0" }, "volta": { - "node": "24.13.1" + "node": "24.14.1" } } diff --git a/e2e/src/api/specs/duplicate.e2e-spec.ts b/e2e/src/api/specs/duplicate.e2e-spec.ts new file mode 100644 index 0000000000..d6d0ec1394 --- /dev/null +++ b/e2e/src/api/specs/duplicate.e2e-spec.ts @@ -0,0 +1,651 @@ +import { LoginResponseDto } from '@immich/sdk'; +import { createUserDto, uuidDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { app, utils } from 'src/utils'; +import request from 'supertest'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; + +describe('/duplicates', () => { + let admin: LoginResponseDto; + let user1: LoginResponseDto; + let user2: LoginResponseDto; + + beforeAll(async () => { + await utils.resetDatabase(); + + admin = await utils.adminSetup(); + + [user1, user2] = await Promise.all([ + utils.userSetup(admin.accessToken, createUserDto.user1), + utils.userSetup(admin.accessToken, createUserDto.user2), + ]); + }); + + beforeEach(async () => { + // Reset assets, albums, tags, and stacks between tests to ensure clean state for repeated test runs + // Note: We don't reset users since they're set up once in beforeAll + // Stack must be reset before asset due to foreign key constraint + await utils.resetDatabase(['stack', 'asset', 'album', 'tag']); + }); + + describe('GET /duplicates', () => { + it('should return empty array when no duplicates', async () => { + const { status, body } = await request(app) + .get('/duplicates') + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual([]); + }); + + it('should return duplicate groups with suggestedKeepAssetIds', async () => { + // Create assets with different file sizes for duplicate detection + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + // Manually set duplicateId on both assets to create a duplicate group + const duplicateId = '00000000-0000-4000-8000-000000000001'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + + const { status, body } = await request(app) + .get('/duplicates') + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual([ + { + duplicateId, + assets: expect.arrayContaining([ + expect.objectContaining({ id: asset1.id }), + expect.objectContaining({ id: asset2.id }), + ]), + suggestedKeepAssetIds: expect.any(Array), + }, + ]); + expect(body[0].suggestedKeepAssetIds.length).toBe(1); + }); + }); + + describe('POST /duplicates/resolve', () => { + it('should require authentication', async () => { + const { status, body } = await request(app) + .post('/duplicates/resolve') + .send({ + groups: [{ duplicateId: uuidDto.dummy, keepAssetIds: [], trashAssetIds: [] }], + }); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should return failure for non-existent duplicate group', async () => { + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId: uuidDto.dummy, keepAssetIds: [], trashAssetIds: [] }], + }); + + expect(status).toBe(200); + expect(body).toEqual({ + status: 'COMPLETED', + results: [ + { + duplicateId: uuidDto.dummy, + status: 'FAILED', + reason: expect.stringContaining('not found or access denied'), + }, + ], + }); + }); + + it('should resolve duplicate group with keepers', async () => { + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + const duplicateId = '00000000-0000-4000-8000-000000000002'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }], + }); + + expect(status).toBe(200); + expect(body).toEqual({ + status: 'COMPLETED', + results: [ + { + duplicateId, + status: 'SUCCESS', + }, + ], + }); + + // Verify side effects: duplicateId cleared on kept asset + const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id); + expect(keptAsset.duplicateId).toBeNull(); + + // Verify side effects: trashed asset is trashed and duplicateId cleared + const trashedAsset = await utils.getAssetInfo(user1.accessToken, asset2.id); + expect(trashedAsset.isTrashed).toBe(true); + expect(trashedAsset.duplicateId).toBeNull(); + }); + + it('should reject when keepAssetIds and trashAssetIds overlap', async () => { + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + const duplicateId = '00000000-0000-4000-8000-000000000003'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset1.id] }], + }); + + expect(status).toBe(200); + expect(body.results[0].status).toBe('FAILED'); + expect(body.results[0].reason).toContain('disjoint'); + }); + + it('should require keepAssetIds when partially trashing', async () => { + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + const duplicateId = '00000000-0000-4000-8000-000000000004'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [], trashAssetIds: [asset1.id] }], + }); + + expect(status).toBe(200); + expect(body.results[0].status).toBe('FAILED'); + expect(body.results[0].reason).toContain('must cover all assets'); + }); + + it('should reject partial resolution (not all assets covered)', async () => { + const [asset1, asset2, asset3] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + const duplicateId = '00000000-0000-4000-8000-000000000010'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset3.id, duplicateId); + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }], + }); + + expect(status).toBe(200); + expect(body.results[0].status).toBe('FAILED'); + expect(body.results[0].reason).toContain('must cover all assets'); + }); + + it('should reject asset not in duplicate group', async () => { + const [asset1, asset2, outsideAsset] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + const duplicateId = '00000000-0000-4000-8000-000000000011'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [outsideAsset.id] }], + }); + + expect(status).toBe(200); + expect(body.results[0].status).toBe('FAILED'); + expect(body.results[0].reason).toContain('not a member of duplicate group'); + }); + + it('should allow trash-all without keepers', async () => { + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + const duplicateId = '00000000-0000-4000-8000-000000000012'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [], trashAssetIds: [asset1.id, asset2.id] }], + }); + + expect(status).toBe(200); + expect(body).toEqual({ + status: 'COMPLETED', + results: [ + { + duplicateId, + status: 'SUCCESS', + }, + ], + }); + + // Verify both assets are trashed + const [asset1Info, asset2Info] = await Promise.all([ + utils.getAssetInfo(user1.accessToken, asset1.id), + utils.getAssetInfo(user1.accessToken, asset2.id), + ]); + + expect(asset1Info.isTrashed).toBe(true); + expect(asset1Info.duplicateId).toBeNull(); + expect(asset2Info.isTrashed).toBe(true); + expect(asset2Info.duplicateId).toBeNull(); + }); + + it('should reject cross-user duplicate group access', async () => { + const asset1 = await utils.createAsset(user1.accessToken); + const asset2 = await utils.createAsset(user2.accessToken); + + const duplicateId = '00000000-0000-4000-8000-000000000013'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user2.accessToken, asset2.id, duplicateId); + + // User1 tries to resolve a group containing user2's asset + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }], + }); + + expect(status).toBe(200); + expect(body.results[0].status).toBe('FAILED'); + expect(body.results[0].reason).toContain('not a member of duplicate group'); + }); + + it('should synchronize favorites when enabled', async () => { + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + // Mark one asset as favorite + await request(app) + .put('/assets') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ ids: [asset2.id], isFavorite: true }); + + const duplicateId = '00000000-0000-4000-8000-000000000020'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }], + }); + + expect(status).toBe(200); + expect(body.results[0].status).toBe('SUCCESS'); + + // Verify favorite was synchronized to keeper + const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id); + expect(keptAsset.isFavorite).toBe(true); + expect(keptAsset.duplicateId).toBeNull(); + }); + + it('should synchronize visibility when enabled', async () => { + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + // Archive one asset + await utils.archiveAssets(user1.accessToken, [asset2.id]); + + const duplicateId = '00000000-0000-4000-8000-000000000021'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }], + }); + + expect(status).toBe(200); + expect(body.results[0].status).toBe('SUCCESS'); + + // Verify visibility was synchronized to keeper + const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id); + expect(keptAsset.visibility).toBe('archive'); + expect(keptAsset.duplicateId).toBeNull(); + }); + + it('should synchronize rating when enabled', async () => { + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + // Set rating on one asset + await request(app) + .put('/assets') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ ids: [asset2.id], rating: 5 }); + + const duplicateId = '00000000-0000-4000-8000-000000000022'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }], + }); + + expect(status).toBe(200); + expect(body.results[0].status).toBe('SUCCESS'); + + // Verify rating was synchronized to keeper + const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id); + expect(keptAsset.exifInfo?.rating).toBe(5); + expect(keptAsset.duplicateId).toBeNull(); + }); + + it('should synchronize description when enabled', async () => { + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + // Set description on one asset + await request(app) + .put('/assets') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ ids: [asset2.id], description: 'Test description for duplicate' }); + + const duplicateId = '00000000-0000-4000-8000-000000000023'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }], + }); + + expect(status).toBe(200); + expect(body.results[0].status).toBe('SUCCESS'); + + // Verify description was synchronized to keeper + const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id); + expect(keptAsset.exifInfo?.description).toBe('Test description for duplicate'); + expect(keptAsset.duplicateId).toBeNull(); + }); + + it('should synchronize location when enabled', async () => { + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + // Set location on one asset + await request(app) + .put('/assets') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ ids: [asset2.id], latitude: 40.7128, longitude: -74.006 }); + + const duplicateId = '00000000-0000-4000-8000-000000000024'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }], + }); + + expect(status).toBe(200); + expect(body.results[0].status).toBe('SUCCESS'); + + // Verify location was synchronized to keeper + const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id); + expect(keptAsset.exifInfo?.latitude).toBe(40.7128); + expect(keptAsset.exifInfo?.longitude).toBe(-74.006); + expect(keptAsset.duplicateId).toBeNull(); + }); + + it('should synchronize albums when enabled', async () => { + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + // Create albums and add assets to different albums + const album1 = await utils.createAlbum(user1.accessToken, { + albumName: 'Album 1', + assetIds: [asset1.id], + }); + const album2 = await utils.createAlbum(user1.accessToken, { + albumName: 'Album 2', + assetIds: [asset2.id], + }); + + const duplicateId = '00000000-0000-4000-8000-000000000025'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }], + }); + + expect(status).toBe(200); + expect(body.results[0].status).toBe('SUCCESS'); + + // Verify keeper is now in both albums + const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id); + expect(keptAsset.duplicateId).toBeNull(); + + // Check albums directly + const { status: album1Status, body: album1Body } = await request(app) + .get(`/albums/${album1.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + const { status: album2Status, body: album2Body } = await request(app) + .get(`/albums/${album2.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(album1Status).toBe(200); + expect(album2Status).toBe(200); + expect(album1Body.assets.map((a: any) => a.id)).toContain(asset1.id); + expect(album2Body.assets.map((a: any) => a.id)).toContain(asset1.id); + }); + + it('should synchronize tags when enabled', async () => { + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + // Wait for metadata extraction to complete before adding tags + // Otherwise, metadata jobs will race and overwrite our tags + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + // Create tags and tag assets differently + const tags = await utils.upsertTags(user1.accessToken, ['tag1', 'tag2']); + await utils.tagAssets(user1.accessToken, tags[0].id, [asset1.id]); + await utils.tagAssets(user1.accessToken, tags[1].id, [asset2.id]); + + const duplicateId = '00000000-0000-4000-8000-000000000026'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }], + }); + + expect(status).toBe(200); + expect(body.results[0].status).toBe('SUCCESS'); + + // Verify keeper has both tags + const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id); + expect(keptAsset.duplicateId).toBeNull(); + expect(keptAsset.tags).toBeDefined(); + const tagIds = keptAsset.tags?.map((t) => t.id) || []; + expect(tagIds).toContain(tags[0].id); + expect(tagIds).toContain(tags[1].id); + }); + + it('should handle batch resolve with mixed success and failure', async () => { + // Create first group that will succeed + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + const duplicateId1 = '00000000-0000-4000-8000-000000000027'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId1); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId1); + + // Create second group with non-existent duplicate ID (will fail) + const fakeId = '00000000-0000-4000-8000-000000000099'; + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [ + { duplicateId: duplicateId1, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }, + { duplicateId: fakeId, keepAssetIds: [], trashAssetIds: [] }, + ], + }); + + expect(status).toBe(200); + expect(body.status).toBe('COMPLETED'); + expect(body.results).toHaveLength(2); + + // First group should succeed + expect(body.results[0].duplicateId).toBe(duplicateId1); + expect(body.results[0].status).toBe('SUCCESS'); + + // Second group should fail + expect(body.results[1].duplicateId).toBe(fakeId); + expect(body.results[1].status).toBe('FAILED'); + expect(body.results[1].reason).toContain('not found or access denied'); + + // Verify first group was actually resolved despite second failure + const asset1Info = await utils.getAssetInfo(user1.accessToken, asset1.id); + expect(asset1Info.duplicateId).toBeNull(); + const asset2Info = await utils.getAssetInfo(user1.accessToken, asset2.id); + expect(asset2Info.isTrashed).toBe(true); + }); + + it('should trash assets when trash is enabled', async () => { + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + const duplicateId = '00000000-0000-4000-8000-000000000028'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + + // Ensure trash is enabled (default) + const config = await utils.getSystemConfig(admin.accessToken); + expect(config.trash.enabled).toBe(true); + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }], + }); + + expect(status).toBe(200); + expect(body.results[0].status).toBe('SUCCESS'); + + // Verify asset is trashed (not deleted) + const trashedAsset = await utils.getAssetInfo(user1.accessToken, asset2.id); + expect(trashedAsset.isTrashed).toBe(true); + }); + + it('should delete assets when trash is disabled', async () => { + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + const duplicateId = '00000000-0000-4000-8000-000000000029'; + await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId); + await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId); + + // Disable trash + await request(app) + .put('/system-config') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ + trash: { enabled: false, days: 30 }, + }); + + const { status, body } = await request(app) + .post('/duplicates/resolve') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ + groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }], + }); + + expect(status).toBe(200); + expect(body.results[0].status).toBe('SUCCESS'); + + // Asset should be marked as deleted (force delete) + const { status: getStatus } = await request(app) + .get(`/assets/${asset2.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + // Asset should still be accessible (soft deleted) but marked as deleted + expect(getStatus).toBe(200); + + // Re-enable trash for other tests + await utils.resetAdminConfig(admin.accessToken); + }); + }); +}); diff --git a/e2e/src/fixtures.ts b/e2e/src/fixtures.ts index 9e311c896d..1e03ad6d24 100644 --- a/e2e/src/fixtures.ts +++ b/e2e/src/fixtures.ts @@ -2,6 +2,8 @@ export const uuidDto = { invalid: 'invalid-uuid', // valid uuid v4 notFound: '00000000-0000-4000-a000-000000000000', + dummy: '00000000-0000-4000-a000-000000000001', + dummy2: '00000000-0000-4000-a000-000000000002', }; const adminLoginDto = { diff --git a/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts b/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts index 2b0f6ae61a..b69bd099ed 100644 --- a/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts +++ b/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts @@ -10,7 +10,9 @@ describe('/admin/database-backups', () => { beforeAll(async () => { await utils.resetDatabase(); - admin = await utils.adminSetup(); + admin = await utils.adminSetup({ + onboarding: false, + }); await utils.resetBackups(admin.accessToken); }); @@ -94,7 +96,9 @@ describe('/admin/database-backups', () => { ({ status, body }) => status === 200 && !body.maintenanceMode, ); - admin = await utils.adminSetup(); + admin = await utils.adminSetup({ + onboarding: false, + }); }); it.sequential('should not work when the server is configured', async () => { diff --git a/e2e/src/specs/server/api/album.e2e-spec.ts b/e2e/src/specs/server/api/album.e2e-spec.ts index c4f06edd93..3725de8d26 100644 --- a/e2e/src/specs/server/api/album.e2e-spec.ts +++ b/e2e/src/specs/server/api/album.e2e-spec.ts @@ -130,12 +130,11 @@ describe('/albums', () => { describe('GET /albums', () => { it("should not show other users' favorites", async () => { const { status, body } = await request(app) - .get(`/albums/${user1Albums[0].id}?withoutAssets=false`) + .get(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user2.accessToken}`); expect(status).toEqual(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining({ isFavorite: false })], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], lastModifiedAssetTimestamp: expect.any(String), startDate: expect.any(String), @@ -304,13 +303,12 @@ describe('/albums', () => { describe('GET /albums/:id', () => { it('should return album info for own album', async () => { const { status, body } = await request(app) - .get(`/albums/${user1Albums[0].id}?withoutAssets=false`) + .get(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], lastModifiedAssetTimestamp: expect.any(String), startDate: expect.any(String), @@ -322,7 +320,7 @@ describe('/albums', () => { it('should return album info for shared album (editor)', async () => { const { status, body } = await request(app) - .get(`/albums/${user2Albums[0].id}?withoutAssets=false`) + .get(`/albums/${user2Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); @@ -331,14 +329,14 @@ describe('/albums', () => { it('should return album info for shared album (viewer)', async () => { const { status, body } = await request(app) - .get(`/albums/${user1Albums[3].id}?withoutAssets=false`) + .get(`/albums/${user1Albums[3].id}`) .set('Authorization', `Bearer ${user2.accessToken}`); expect(status).toBe(200); expect(body).toMatchObject({ id: user1Albums[3].id }); }); - it('should return album info with assets when withoutAssets is undefined', async () => { + it('should return album info', async () => { const { status, body } = await request(app) .get(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); @@ -346,25 +344,6 @@ describe('/albums', () => { expect(status).toBe(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], - contributorCounts: [{ userId: user1.userId, assetCount: 1 }], - lastModifiedAssetTimestamp: expect.any(String), - startDate: expect.any(String), - endDate: expect.any(String), - albumUsers: expect.any(Array), - shared: true, - }); - }); - - it('should return album info without assets when withoutAssets is true', async () => { - const { status, body } = await request(app) - .get(`/albums/${user1Albums[0].id}?withoutAssets=true`) - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual({ - ...user1Albums[0], - assets: [], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], assetCount: 1, lastModifiedAssetTimestamp: expect.any(String), @@ -379,13 +358,12 @@ describe('/albums', () => { await utils.deleteAssets(user1.accessToken, [user1Asset2.id]); const { status, body } = await request(app) - .get(`/albums/${user2Albums[0].id}?withoutAssets=true`) + .get(`/albums/${user2Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toEqual({ ...user2Albums[0], - assets: [], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], assetCount: 1, lastModifiedAssetTimestamp: expect.any(String), @@ -426,7 +404,6 @@ describe('/albums', () => { shared: false, albumUsers: [], hasSharedLink: false, - assets: [], assetCount: 0, owner: expect.objectContaining({ email: user1.userEmail }), isActivityEnabled: true, @@ -524,14 +501,19 @@ describe('/albums', () => { expect(body).toEqual(errorDto.badRequest('Not found or no album.update access')); }); - it('should not be able to update as an editor', async () => { + it('should be able to update as an editor', async () => { const { status, body } = await request(app) .patch(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user2.accessToken}`) .send({ albumName: 'New album name' }); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest('Not found or no album.update access')); + expect(status).toBe(200); + expect(body).toEqual( + expect.objectContaining({ + id: user1Albums[0].id, + albumName: 'New album name', + }), + ); }); }); diff --git a/e2e/src/specs/server/api/asset.e2e-spec.ts b/e2e/src/specs/server/api/asset.e2e-spec.ts index 11e825a7cd..3fbacd5bf6 100644 --- a/e2e/src/specs/server/api/asset.e2e-spec.ts +++ b/e2e/src/specs/server/api/asset.e2e-spec.ts @@ -1,7 +1,6 @@ import { AssetMediaResponseDto, AssetMediaStatus, - AssetResponseDto, AssetTypeEnum, AssetVisibility, getAssetInfo, @@ -19,7 +18,7 @@ import { Socket } from 'socket.io-client'; import { createUserDto, uuidDto } from 'src/fixtures'; import { makeRandomImage } from 'src/generators'; import { errorDto } from 'src/responses'; -import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/utils'; +import { app, asBearerAuth, tempDir, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -95,8 +94,8 @@ describe('/asset', () => { utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken, { isFavorite: true, - fileCreatedAt: yesterday.toISO(), - fileModifiedAt: yesterday.toISO(), + fileCreatedAt: yesterday.toUTC().toISO(), + fileModifiedAt: yesterday.toUTC().toISO(), assetData: { filename: 'example.mp4' }, }), utils.createAsset(user1.accessToken), @@ -380,62 +379,12 @@ describe('/asset', () => { }); }); - describe('GET /assets/random', () => { - beforeAll(async () => { - await Promise.all([ - utils.createAsset(user1.accessToken), - utils.createAsset(user1.accessToken), - utils.createAsset(user1.accessToken), - utils.createAsset(user1.accessToken), - utils.createAsset(user1.accessToken), - utils.createAsset(user1.accessToken), - ]); - - await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); - }); - - it.each(TEN_TIMES)('should return 1 random assets', async () => { - const { status, body } = await request(app) - .get('/assets/random') - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - - const assets: AssetResponseDto[] = body; - expect(assets.length).toBe(1); - expect(assets[0].ownerId).toBe(user1.userId); - }); - - it.each(TEN_TIMES)('should return 2 random assets', async () => { - const { status, body } = await request(app) - .get('/assets/random?count=2') - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - - const assets: AssetResponseDto[] = body; - expect(assets.length).toBe(2); - - for (const asset of assets) { - expect(asset.ownerId).toBe(user1.userId); - } - }); - - it.skip('should return 1 asset if there are 10 assets in the database but user 2 only has 1', async () => { - const { status, body } = await request(app) - .get('/assets/random') - .set('Authorization', `Bearer ${user2.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]); - }); - }); - describe('PUT /assets/:id', () => { it('should require access', async () => { const { status, body } = await request(app) .put(`/assets/${user2Assets[0].id}`) - .set('Authorization', `Bearer ${user1.accessToken}`); + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({}); expect(status).toBe(400); expect(body).toEqual(errorDto.noPermission); }); @@ -1142,8 +1091,6 @@ describe('/asset', () => { const { body, status } = await request(app) .post('/assets') .set('Authorization', `Bearer ${quotaUser.accessToken}`) - .field('deviceAssetId', 'example-image') - .field('deviceId', 'e2e') .field('fileCreatedAt', new Date().toISOString()) .field('fileModifiedAt', new Date().toISOString()) .attach('assetData', makeRandomImage(), 'example.jpg'); @@ -1160,8 +1107,6 @@ describe('/asset', () => { const { body, status } = await request(app) .post('/assets') .set('Authorization', `Bearer ${quotaUser.accessToken}`) - .field('deviceAssetId', 'example-image') - .field('deviceId', 'e2e') .field('fileCreatedAt', new Date().toISOString()) .field('fileModifiedAt', new Date().toISOString()) .attach('assetData', randomBytes(2014), 'example.jpg'); @@ -1215,29 +1160,4 @@ describe('/asset', () => { expect(video.checksum).toStrictEqual(checksum); }); }); - - describe('POST /assets/exist', () => { - it('ignores invalid deviceAssetIds', async () => { - const response = await utils.checkExistingAssets(user1.accessToken, { - deviceId: 'test-assets-exist', - deviceAssetIds: ['invalid', 'INVALID'], - }); - - expect(response.existingIds).toHaveLength(0); - }); - - it('returns the IDs of existing assets', async () => { - await utils.createAsset(user1.accessToken, { - deviceId: 'test-assets-exist', - deviceAssetId: 'test-asset-0', - }); - - const response = await utils.checkExistingAssets(user1.accessToken, { - deviceId: 'test-assets-exist', - deviceAssetIds: ['test-asset-0'], - }); - - expect(response.existingIds).toEqual(['test-asset-0']); - }); - }); }); diff --git a/e2e/src/specs/server/api/library.e2e-spec.ts b/e2e/src/specs/server/api/library.e2e-spec.ts index 4d67a84647..719436a66d 100644 --- a/e2e/src/specs/server/api/library.e2e-spec.ts +++ b/e2e/src/specs/server/api/library.e2e-spec.ts @@ -110,7 +110,7 @@ describe('/libraries', () => { }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items'])); }); it('should not create an external library with duplicate exclusion patterns', async () => { @@ -125,7 +125,7 @@ describe('/libraries', () => { }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items'])); }); }); @@ -157,7 +157,7 @@ describe('/libraries', () => { .send({ name: '' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['name should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[name] Too small: expected string to have >=1 characters'])); }); it('should change the import paths', async () => { @@ -181,7 +181,7 @@ describe('/libraries', () => { .send({ importPaths: [''] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in importPaths should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[importPaths] Array items must not be empty'])); }); it('should reject duplicate import paths', async () => { @@ -191,7 +191,7 @@ describe('/libraries', () => { .send({ importPaths: ['/path', '/path'] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items'])); }); it('should change the exclusion pattern', async () => { @@ -215,7 +215,7 @@ describe('/libraries', () => { .send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items'])); }); it('should reject an empty exclusion pattern', async () => { @@ -225,7 +225,7 @@ describe('/libraries', () => { .send({ exclusionPatterns: [''] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in exclusionPatterns should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array items must not be empty'])); }); }); diff --git a/e2e/src/specs/server/api/map.e2e-spec.ts b/e2e/src/specs/server/api/map.e2e-spec.ts index 977638aa24..c280deb134 100644 --- a/e2e/src/specs/server/api/map.e2e-spec.ts +++ b/e2e/src/specs/server/api/map.e2e-spec.ts @@ -109,7 +109,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lon=123') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); + expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN'])); }); it('should throw an error if a lat is not a number', async () => { @@ -117,7 +117,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lat=abc&lon=123.456') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); + expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN'])); }); it('should throw an error if a lat is out of range', async () => { @@ -125,7 +125,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lat=91&lon=123.456') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); + expect(body).toEqual(errorDto.badRequest(['[lat] Too big: expected number to be <=90'])); }); it('should throw an error if a lon is not provided', async () => { @@ -133,7 +133,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lat=75') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lon must be a number between -180 and 180'])); + expect(body).toEqual(errorDto.badRequest(['[lon] Invalid input: expected number, received NaN'])); }); const reverseGeocodeTestCases = [ diff --git a/e2e/src/specs/server/api/oauth.e2e-spec.ts b/e2e/src/specs/server/api/oauth.e2e-spec.ts index ae9064375f..9dcb431a4b 100644 --- a/e2e/src/specs/server/api/oauth.e2e-spec.ts +++ b/e2e/src/specs/server/api/oauth.e2e-spec.ts @@ -1,9 +1,10 @@ -import { OAuthClient, OAuthUser } from '@immich/e2e-auth-server'; +import { OAuthClient, OAuthUser, generateLogoutToken } from '@immich/e2e-auth-server'; import { LoginResponseDto, SystemConfigOAuthDto, getConfigDefaults, getMyUser, + getSessions, startOAuth, updateConfig, } from '@immich/sdk'; @@ -76,6 +77,7 @@ const setupOAuth = async (token: string, dto: Partial) => ...defaults.oauth, buttonText: 'Login with Immich', issuerUrl: `${authServer.internal}/.well-known/openid-configuration`, + allowInsecureRequests: true, ...dto, }; await updateConfig({ systemConfigDto: { ...defaults, oauth: merged } }, options); @@ -87,21 +89,23 @@ describe(`/oauth`, () => { beforeAll(async () => { await utils.resetDatabase(); admin = await utils.adminSetup(); - - await setupOAuth(admin.accessToken, { - enabled: true, - clientId: OAuthClient.DEFAULT, - clientSecret: OAuthClient.DEFAULT, - buttonText: 'Login with Immich', - storageLabelClaim: 'immich_username', - }); }); describe('POST /oauth/authorize', () => { + beforeAll(async () => { + await setupOAuth(admin.accessToken, { + enabled: true, + clientId: OAuthClient.DEFAULT, + clientSecret: OAuthClient.DEFAULT, + buttonText: 'Login with Immich', + storageLabelClaim: 'immich_username', + }); + }); + it(`should throw an error if a redirect uri is not provided`, async () => { const { status, body } = await request(app).post('/oauth/authorize').send({}); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['redirectUri must be a string', 'redirectUri should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[redirectUri] Invalid input: expected string, received undefined'])); }); it('should return a redirect uri', async () => { @@ -117,19 +121,56 @@ describe(`/oauth`, () => { expect(params.get('redirect_uri')).toBe('http://127.0.0.1:2285/auth/login'); expect(params.get('state')).toBeDefined(); }); + + it('should not include the prompt parameter when not configured', async () => { + const { status, body } = await request(app) + .post('/oauth/authorize') + .send({ redirectUri: 'http://127.0.0.1:2285/auth/login' }); + expect(status).toBe(201); + + const params = new URL(body.url).searchParams; + expect(params.get('prompt')).toBeNull(); + }); + + it('should include the prompt parameter when configured', async () => { + await setupOAuth(admin.accessToken, { + enabled: true, + clientId: OAuthClient.DEFAULT, + clientSecret: OAuthClient.DEFAULT, + prompt: 'select_account', + }); + + const { status, body } = await request(app) + .post('/oauth/authorize') + .send({ redirectUri: 'http://127.0.0.1:2285/auth/login' }); + expect(status).toBe(201); + + const params = new URL(body.url).searchParams; + expect(params.get('prompt')).toBe('select_account'); + }); }); describe('POST /oauth/callback', () => { + beforeAll(async () => { + await setupOAuth(admin.accessToken, { + enabled: true, + clientId: OAuthClient.DEFAULT, + clientSecret: OAuthClient.DEFAULT, + buttonText: 'Login with Immich', + storageLabelClaim: 'immich_username', + }); + }); + it(`should throw an error if a url is not provided`, async () => { const { status, body } = await request(app).post('/oauth/callback').send({}); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['url must be a string', 'url should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[url] Invalid input: expected string, received undefined'])); }); it(`should throw an error if the url is empty`, async () => { const { status, body } = await request(app).post('/oauth/callback').send({ url: '' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['url should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[url] Too small: expected string to have >=1 characters'])); }); it(`should throw an error if the state is not provided`, async () => { @@ -158,10 +199,9 @@ describe(`/oauth`, () => { it(`should throw an error if the codeVerifier doesn't match the challenge`, async () => { const callbackParams = await loginWithOAuth('oauth-auto-register'); const { codeVerifier } = await loginWithOAuth('oauth-auto-register'); - const { status, body } = await request(app) + const { status } = await request(app) .post('/oauth/callback') .send({ ...callbackParams, codeVerifier }); - console.log(body); expect(status).toBeGreaterThanOrEqual(400); }); @@ -258,7 +298,7 @@ describe(`/oauth`, () => { accessToken: expect.any(String), isAdmin: false, name: 'OAuth User', - userEmail: 'oauth-RS256-token@immich.app', + userEmail: 'oauth-rs256-token@immich.app', userId: expect.any(String), }); }); @@ -333,6 +373,50 @@ describe(`/oauth`, () => { }); }); + describe(`POST /oauth/backchannel-logout`, () => { + it(`should throw an error if the logout_token is not provided`, async () => { + const { status, body } = await request(app).post('/oauth/backchannel-logout').send({}); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['[logout_token] Invalid input: expected string, received undefined'])); + }); + + it(`should throw an error if an invalid logout token is provided`, async () => { + const { status, body } = await request(app) + .post('/oauth/backchannel-logout') + .send({ logout_token: 'invalid token' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('Error backchannel logout: token validation failed')); + }); + + it(`should logout user if a valid logout token is provided`, async () => { + await setupOAuth(admin.accessToken, { + enabled: true, + clientId: OAuthClient.DEFAULT, + clientSecret: OAuthClient.DEFAULT, + autoRegister: true, + signingAlgorithm: 'RS256', + buttonText: 'Login with Immich', + }); + + const callbackParams = await loginWithOAuth('backchannel-logout-user'); + const { status: callbackStatus, body: callbackBody } = await request(app) + .post('/oauth/callback') + .send(callbackParams); + expect(callbackStatus).toBe(201); + + await expect(getSessions({ headers: asBearerAuth(callbackBody.accessToken) })).resolves.toHaveLength(1); + + const logoutToken = await generateLogoutToken('http://0.0.0.0:2286', 'backchannel-logout-user'); + const { status, body } = await request(app).post('/oauth/backchannel-logout').send({ logout_token: logoutToken }); + expect(status).toBe(200); + expect(body).toMatchObject({}); + + await expect(getSessions({ headers: asBearerAuth(callbackBody.accessToken) })).rejects.toMatchObject({ + status: 401, + }); + }); + }); + describe('mobile redirect override', () => { beforeAll(async () => { await setupOAuth(admin.accessToken, { @@ -399,4 +483,23 @@ describe(`/oauth`, () => { }); }); }); + + describe('allowInsecureRequests: false', () => { + beforeAll(async () => { + await setupOAuth(admin.accessToken, { + enabled: true, + clientId: OAuthClient.DEFAULT, + clientSecret: OAuthClient.DEFAULT, + allowInsecureRequests: false, + }); + }); + + it('should reject OAuth discovery over HTTP', async () => { + const { status, body } = await request(app) + .post('/oauth/authorize') + .send({ redirectUri: 'http://127.0.0.1:2285/auth/login' }); + expect(status).toBe(500); + expect(body).toMatchObject({ statusCode: 500 }); + }); + }); }); diff --git a/e2e/src/specs/server/api/search.e2e-spec.ts b/e2e/src/specs/server/api/search.e2e-spec.ts index 2f6ea75f77..e3e17f67c2 100644 --- a/e2e/src/specs/server/api/search.e2e-spec.ts +++ b/e2e/src/specs/server/api/search.e2e-spec.ts @@ -74,7 +74,6 @@ describe('/search', () => { const bytes = await readFile(join(testAssetDir, filename)); assets.push( await utils.createAsset(admin.accessToken, { - deviceAssetId: `test-${filename}`, assetData: { bytes, filename }, ...dto, }), @@ -458,7 +457,7 @@ describe('/search', () => { expect(Array.isArray(body)).toBe(true); if (Array.isArray(body)) { expect(body.length).toBeGreaterThan(10); - expect(body[0].name).toEqual(name); + expect(body[0].name).toEqual(expect.stringContaining(name)); expect(body[0].admin2name).toEqual(name); } }); diff --git a/e2e/src/specs/server/api/server.e2e-spec.ts b/e2e/src/specs/server/api/server.e2e-spec.ts index 3dd6f15e71..1220e6cab5 100644 --- a/e2e/src/specs/server/api/server.e2e-spec.ts +++ b/e2e/src/specs/server/api/server.e2e-spec.ts @@ -207,16 +207,6 @@ describe('/server', () => { }); }); - describe('GET /server/theme', () => { - it('should respond with the server theme', async () => { - const { status, body } = await request(app).get('/server/theme'); - expect(status).toBe(200); - expect(body).toEqual({ - customCss: '', - }); - }); - }); - describe('GET /server/license', () => { it('should require authentication', async () => { const { status, body } = await request(app).get('/server/license'); diff --git a/e2e/src/specs/server/api/shared-link.e2e-spec.ts b/e2e/src/specs/server/api/shared-link.e2e-spec.ts index 00c455d6cb..1d069d0f54 100644 --- a/e2e/src/specs/server/api/shared-link.e2e-spec.ts +++ b/e2e/src/specs/server/api/shared-link.e2e-spec.ts @@ -243,9 +243,21 @@ describe('/shared-links', () => { }); it('should get data for correct password protected link', async () => { + const response = await request(app) + .post('/shared-links/login') + .send({ password: 'foo' }) + .query({ key: linkWithPassword.key }); + + expect(response.status).toBe(201); + + const cookies = response.get('Set-Cookie') ?? []; + expect(cookies).toHaveLength(1); + expect(cookies[0]).toContain('immich_shared_link_token'); + const { status, body } = await request(app) .get('/shared-links/me') - .query({ key: linkWithPassword.key, password: 'foo' }); + .query({ key: linkWithPassword.key }) + .set('Cookie', cookies); expect(status).toBe(200); expect(body).toEqual( diff --git a/e2e/src/specs/server/api/tag.e2e-spec.ts b/e2e/src/specs/server/api/tag.e2e-spec.ts index d69536f3a3..7b5a2f16de 100644 --- a/e2e/src/specs/server/api/tag.e2e-spec.ts +++ b/e2e/src/specs/server/api/tag.e2e-spec.ts @@ -309,7 +309,7 @@ describe('/tags', () => { .get(`/tags/${uuidDto.invalid}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); it('should get tag details', async () => { @@ -427,7 +427,7 @@ describe('/tags', () => { .delete(`/tags/${uuidDto.invalid}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); it('should delete a tag', async () => { diff --git a/e2e/src/specs/server/api/user-admin.e2e-spec.ts b/e2e/src/specs/server/api/user-admin.e2e-spec.ts index 793c508a36..6751b21e84 100644 --- a/e2e/src/specs/server/api/user-admin.e2e-spec.ts +++ b/e2e/src/specs/server/api/user-admin.e2e-spec.ts @@ -287,7 +287,8 @@ describe('/admin/users', () => { it('should delete user', async () => { const { status, body } = await request(app) .delete(`/admin/users/${userToDelete.userId}`) - .set('Authorization', `Bearer ${admin.accessToken}`); + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({}); expect(status).toBe(200); expect(body).toMatchObject({ diff --git a/e2e/src/specs/server/api/user.e2e-spec.ts b/e2e/src/specs/server/api/user.e2e-spec.ts index 3f280dddf5..ee13a29c1b 100644 --- a/e2e/src/specs/server/api/user.e2e-spec.ts +++ b/e2e/src/specs/server/api/user.e2e-spec.ts @@ -178,7 +178,9 @@ describe('/users', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['download.archiveSize must be an integer number'])); + expect(body).toEqual( + errorDto.badRequest(['[download.archiveSize] Invalid input: expected int, received number']), + ); }); it('should update download archive size', async () => { @@ -204,7 +206,9 @@ describe('/users', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['download.includeEmbeddedVideos must be a boolean value'])); + expect(body).toEqual( + errorDto.badRequest(['[download.includeEmbeddedVideos] Invalid input: expected boolean, received number']), + ); }); it('should update download include embedded videos', async () => { diff --git a/e2e/src/specs/web/album.e2e-spec.ts b/e2e/src/specs/web/album.e2e-spec.ts index 953c7d00ae..cd8bb87582 100644 --- a/e2e/src/specs/web/album.e2e-spec.ts +++ b/e2e/src/specs/web/album.e2e-spec.ts @@ -1,6 +1,7 @@ import { LoginResponseDto } from '@immich/sdk'; -import { test } from '@playwright/test'; -import { utils } from 'src/utils'; +import { expect, test } from '@playwright/test'; +import { readFileSync } from 'node:fs'; +import { testAssetDir, utils } from 'src/utils'; test.describe('Album', () => { let admin: LoginResponseDto; @@ -22,4 +23,41 @@ test.describe('Album', () => { await page.reload(); await page.getByRole('button', { name: 'Select photos' }).waitFor(); }); + + test('should keep map view open after viewing an asset from the map and going back', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + + const imagePath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`; + const mapAsset = await utils.createAsset(admin.accessToken, { + assetData: { + bytes: readFileSync(imagePath), + filename: 'thompson-springs.jpg', + }, + }); + + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + const mapAlbum = await utils.createAlbum(admin.accessToken, { + albumName: 'Map Test Album', + assetIds: [mapAsset.id], + }); + + await page.goto(`/albums/${mapAlbum.id}`); + const mapButton = page.getByRole('button', { name: 'Map' }); + await expect(mapButton).toBeVisible(); + await mapButton.click(); + + const mapModal = page.getByRole('dialog'); + await expect(mapModal).toBeVisible(); + + const mapMarker = mapModal.getByRole('img', { name: /Map marker/i }).first(); + await expect(mapMarker).toBeVisible(); + await mapMarker.click(); + + await page.waitForSelector('#immich-asset-viewer'); + await page.getByRole('button', { name: 'Go back' }).click(); + + await expect(page.locator('#immich-asset-viewer')).not.toBeVisible(); + await expect(mapModal).toBeVisible(); + }); }); diff --git a/e2e/src/specs/web/asset-viewer/detail-panel.e2e-spec.ts b/e2e/src/specs/web/asset-viewer/detail-panel.e2e-spec.ts index 2f90e4e3d8..bbe0ef328f 100644 --- a/e2e/src/specs/web/asset-viewer/detail-panel.e2e-spec.ts +++ b/e2e/src/specs/web/asset-viewer/detail-panel.e2e-spec.ts @@ -1,7 +1,9 @@ import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk'; import { expect, test } from '@playwright/test'; +import { readFile } from 'node:fs/promises'; +import { basename, join } from 'node:path'; import type { Socket } from 'socket.io-client'; -import { utils } from 'src/utils'; +import { testAssetDir, utils } from 'src/utils'; test.describe('Detail Panel', () => { let admin: LoginResponseDto; @@ -83,4 +85,42 @@ test.describe('Detail Panel', () => { await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id }); await expect(textarea).toHaveValue('new description'); }); + + test.describe('Date editor', () => { + test('displays inferred asset timezone', async ({ context, page }) => { + const test = { + filepath: 'metadata/dates/datetimeoriginal-gps.jpg', + expected: { + dateTime: '2025-12-01T11:30', + // Test with a timezone which is NOT the first among timezones with the same offset + // This is to check that the editor does not simply fall back to the first available timezone with that offset + // America/Denver (-07:00) is not the first among timezones with offset -07:00 + timeZoneWithOffset: 'America/Denver (-07:00)', + }, + }; + + const asset = await utils.createAsset(admin.accessToken, { + assetData: { + bytes: await readFile(join(testAssetDir, test.filepath)), + filename: basename(test.filepath), + }, + }); + + await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id }); + + // asset viewer -> detail panel -> date editor + await utils.setAuthCookies(context, admin.accessToken); + await page.goto(`/photos/${asset.id}`); + await page.waitForSelector('#immich-asset-viewer'); + + await page.getByRole('button', { name: 'Info' }).click(); + await page.getByTestId('detail-panel-edit-date-button').click(); + await page.waitForSelector('[role="dialog"]'); + + const datetime = page.locator('#datetime'); + await expect(datetime).toHaveValue(test.expected.dateTime); + const timezone = page.getByRole('combobox', { name: 'Timezone' }); + await expect(timezone).toHaveValue(test.expected.timeZoneWithOffset); + }); + }); }); diff --git a/e2e/src/specs/web/duplicates.e2e-spec.ts b/e2e/src/specs/web/duplicates.e2e-spec.ts new file mode 100644 index 0000000000..c39e9019d3 --- /dev/null +++ b/e2e/src/specs/web/duplicates.e2e-spec.ts @@ -0,0 +1,51 @@ +import { AssetMediaResponseDto, LoginResponseDto, updateAssets } from '@immich/sdk'; +import { expect, test } from '@playwright/test'; +import crypto from 'node:crypto'; +import { asBearerAuth, utils } from 'src/utils'; + +test.describe('Duplicates Utility', () => { + let admin: LoginResponseDto; + let firstAsset: AssetMediaResponseDto; + let secondAsset: AssetMediaResponseDto; + + test.beforeAll(async () => { + utils.initSdk(); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + }); + + test.beforeEach(async ({ context }) => { + [firstAsset, secondAsset] = await Promise.all([ + utils.createAsset(admin.accessToken, {}), + utils.createAsset(admin.accessToken, {}), + ]); + + await updateAssets( + { + assetBulkUpdateDto: { + ids: [firstAsset.id, secondAsset.id], + duplicateId: crypto.randomUUID(), + }, + }, + { headers: asBearerAuth(admin.accessToken) }, + ); + + await utils.setAuthCookies(context, admin.accessToken); + }); + + test('navigates with arrow keys between duplicate preview assets', async ({ page }) => { + await page.goto('/utilities/duplicates'); + await page.getByRole('button', { name: 'View' }).first().click(); + await page.waitForSelector('#immich-asset-viewer'); + + const getViewedAssetId = () => new URL(page.url()).pathname.split('/').at(-1) ?? ''; + const initialAssetId = getViewedAssetId(); + expect([firstAsset.id, secondAsset.id]).toContain(initialAssetId); + + await page.keyboard.press('ArrowRight'); + await expect.poll(getViewedAssetId).not.toBe(initialAssetId); + + await page.keyboard.press('ArrowLeft'); + await expect.poll(getViewedAssetId).toBe(initialAssetId); + }); +}); diff --git a/e2e/src/specs/web/photo-viewer.e2e-spec.ts b/e2e/src/specs/web/photo-viewer.e2e-spec.ts index 76d9d61ed6..71f2145be8 100644 --- a/e2e/src/specs/web/photo-viewer.e2e-spec.ts +++ b/e2e/src/specs/web/photo-viewer.e2e-spec.ts @@ -77,18 +77,4 @@ test.describe('Photo Viewer', () => { }); expect(tagAtCenter).toBe('IMG'); }); - - test('reloads photo when checksum changes', async ({ page }) => { - await page.goto(`/photos/${asset.id}`); - - const preview = page.getByTestId('preview').filter({ visible: true }); - await expect(preview).toHaveAttribute('src', /.+/); - const initialSrc = await preview.getAttribute('src'); - - const websocketEvent = utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id }); - await utils.replaceAsset(admin.accessToken, asset.id); - await websocketEvent; - - await expect(preview).not.toHaveAttribute('src', initialSrc!); - }); }); diff --git a/e2e/src/ui/generators/timeline/rest-response.ts b/e2e/src/ui/generators/timeline/rest-response.ts index 0c4bd06dc3..8fc9ce331d 100644 --- a/e2e/src/ui/generators/timeline/rest-response.ts +++ b/e2e/src/ui/generators/timeline/rest-response.ts @@ -315,11 +315,9 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons return { id: asset.id, - deviceAssetId: `device-${asset.id}`, ownerId: asset.ownerId, owner: owner || defaultOwner, libraryId: `library-${asset.ownerId}`, - deviceId: `device-${asset.ownerId}`, type: asset.isVideo ? AssetTypeEnum.Video : AssetTypeEnum.Image, originalPath: `/original/${asset.id}.${asset.isVideo ? 'mp4' : 'jpg'}`, originalFileName: `${asset.id}.${asset.isVideo ? 'mp4' : 'jpg'}`, @@ -334,7 +332,7 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons isArchived: false, isTrashed: asset.isTrashed, visibility: asset.visibility, - duration: asset.duration || '0:00:00.00000', + duration: asset.duration, exifInfo, livePhotoVideoId: asset.livePhotoVideoId, tags: [], @@ -429,7 +427,6 @@ export function getAlbum( hasSharedLink: false, isActivityEnabled: true, assetCount: albumAssets.length, - assets: albumAssets, startDate: albumAssets.length > 0 ? albumAssets.at(-1)?.fileCreatedAt : undefined, endDate: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined, lastModifiedAssetTimestamp: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined, diff --git a/e2e/src/ui/mock-network/base-network.ts b/e2e/src/ui/mock-network/base-network.ts index f23202ca77..7c4aee59e3 100644 --- a/e2e/src/ui/mock-network/base-network.ts +++ b/e2e/src/ui/mock-network/base-network.ts @@ -1,5 +1,5 @@ import { BrowserContext } from '@playwright/test'; -import { playwrightHost } from 'playwright.config'; +import { playwrightHost } from 'src/../playwright.config'; export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserId: string) => { await context.addCookies([ @@ -173,6 +173,7 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI '.mpeg', '.mpg', '.mts', + '.ts', '.vob', '.webm', '.wmv', diff --git a/e2e/src/ui/mock-network/broken-asset-network.ts b/e2e/src/ui/mock-network/broken-asset-network.ts index 1494b40531..ce66412e61 100644 --- a/e2e/src/ui/mock-network/broken-asset-network.ts +++ b/e2e/src/ui/mock-network/broken-asset-network.ts @@ -16,7 +16,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => { const now = new Date().toISOString(); return { id: assetId, - deviceAssetId: `device-${assetId}`, ownerId, owner: { id: ownerId, @@ -27,7 +26,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => { avatarColor: 'blue' as never, }, libraryId: `library-${ownerId}`, - deviceId: `device-${ownerId}`, type: AssetTypeEnum.Image, originalPath: `/original/${assetId}.jpg`, originalFileName: `${assetId}.jpg`, @@ -42,7 +40,7 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => { isArchived: false, isTrashed: false, visibility: AssetVisibility.Timeline, - duration: '0:00:00.00000', + duration: null, exifInfo: { make: null, model: null, @@ -69,7 +67,7 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => { tags: [], people: [], unassignedFaces: [], - stack: null, + stack: undefined, isOffline: false, hasMetadata: true, duplicateId: null, diff --git a/e2e/src/ui/mock-network/ocr-network.ts b/e2e/src/ui/mock-network/ocr-network.ts new file mode 100644 index 0000000000..3b1a2fe62e --- /dev/null +++ b/e2e/src/ui/mock-network/ocr-network.ts @@ -0,0 +1,55 @@ +import { faker } from '@faker-js/faker'; +import type { AssetOcrResponseDto } from '@immich/sdk'; +import { BrowserContext } from '@playwright/test'; + +export type MockOcrBox = { + text: string; + x1: number; + y1: number; + x2: number; + y2: number; + x3: number; + y3: number; + x4: number; + y4: number; +}; + +export const createMockOcrData = (assetId: string, boxes: MockOcrBox[]): AssetOcrResponseDto[] => { + return boxes.map((box) => ({ + id: faker.string.uuid(), + assetId, + x1: box.x1, + y1: box.y1, + x2: box.x2, + y2: box.y2, + x3: box.x3, + y3: box.y3, + x4: box.x4, + y4: box.y4, + boxScore: 0.95, + textScore: 0.9, + text: box.text, + })); +}; + +export const setupOcrMockApiRoutes = async ( + context: BrowserContext, + ocrDataByAssetId: Map, +) => { + await context.route('**/assets/*/ocr', async (route, request) => { + if (request.method() !== 'GET') { + return route.fallback(); + } + const url = new URL(request.url()); + const segments = url.pathname.split('/'); + const assetIdIndex = segments.indexOf('assets') + 1; + const assetId = segments[assetIdIndex]; + + const ocrData = ocrDataByAssetId.get(assetId) ?? []; + return route.fulfill({ + status: 200, + contentType: 'application/json', + json: ocrData, + }); + }); +}; diff --git a/e2e/src/ui/specs/asset-viewer/ocr.e2e-spec.ts b/e2e/src/ui/specs/asset-viewer/ocr.e2e-spec.ts new file mode 100644 index 0000000000..5a442a6081 --- /dev/null +++ b/e2e/src/ui/specs/asset-viewer/ocr.e2e-spec.ts @@ -0,0 +1,300 @@ +import type { AssetOcrResponseDto, AssetResponseDto } from '@immich/sdk'; +import { expect, test } from '@playwright/test'; +import { toAssetResponseDto } from 'src/ui/generators/timeline'; +import { + createMockStack, + createMockStackAsset, + MockStack, + setupBrokenAssetMockApiRoutes, +} from 'src/ui/mock-network/broken-asset-network'; +import { createMockOcrData, setupOcrMockApiRoutes } from 'src/ui/mock-network/ocr-network'; +import { assetViewerUtils } from '../timeline/utils'; +import { setupAssetViewerFixture } from './utils'; + +test.describe.configure({ mode: 'parallel' }); + +const PRIMARY_OCR_BOXES = [ + { text: 'Hello World', x1: 0.1, y1: 0.1, x2: 0.4, y2: 0.1, x3: 0.4, y3: 0.15, x4: 0.1, y4: 0.15 }, + { text: 'Immich Photo', x1: 0.2, y1: 0.3, x2: 0.6, y2: 0.3, x3: 0.6, y3: 0.36, x4: 0.2, y4: 0.36 }, +]; + +const SECONDARY_OCR_BOXES = [ + { text: 'Second Asset Text', x1: 0.15, y1: 0.2, x2: 0.55, y2: 0.2, x3: 0.55, y3: 0.26, x4: 0.15, y4: 0.26 }, +]; + +test.describe('OCR bounding boxes', () => { + const fixture = setupAssetViewerFixture(920); + + test.beforeEach(async ({ context }) => { + const primaryAssetDto = toAssetResponseDto(fixture.primaryAsset); + const ocrDataByAssetId = new Map([ + [primaryAssetDto.id, createMockOcrData(primaryAssetDto.id, PRIMARY_OCR_BOXES)], + ]); + + await setupOcrMockApiRoutes(context, ocrDataByAssetId); + }); + + test('OCR bounding boxes appear when clicking OCR button', async ({ page }) => { + await page.goto(`/photos/${fixture.primaryAsset.id}`); + await assetViewerUtils.waitForViewerLoad(page, fixture.primaryAsset); + + const ocrButton = page.getByLabel('Text recognition'); + await expect(ocrButton).toBeVisible(); + await ocrButton.click(); + + const ocrBoxes = page.locator('[data-viewer-content] [data-testid="ocr-box"]'); + await expect(ocrBoxes).toHaveCount(2); + + await expect(ocrBoxes.nth(0)).toContainText('Hello World'); + await expect(ocrBoxes.nth(1)).toContainText('Immich Photo'); + }); + + test('OCR bounding boxes toggle off on second click', async ({ page }) => { + await page.goto(`/photos/${fixture.primaryAsset.id}`); + await assetViewerUtils.waitForViewerLoad(page, fixture.primaryAsset); + + const ocrButton = page.getByLabel('Text recognition'); + await ocrButton.click(); + await expect(page.locator('[data-viewer-content] [data-testid="ocr-box"]').first()).toBeVisible(); + + await ocrButton.click(); + await expect(page.locator('[data-viewer-content] [data-testid="ocr-box"]')).toHaveCount(0); + }); +}); + +test.describe('OCR with stacked assets', () => { + const fixture = setupAssetViewerFixture(921); + let mockStack: MockStack; + let primaryAssetDto: AssetResponseDto; + let secondAssetDto: AssetResponseDto; + + test.beforeAll(async () => { + primaryAssetDto = toAssetResponseDto(fixture.primaryAsset); + secondAssetDto = createMockStackAsset(fixture.adminUserId); + secondAssetDto.originalFileName = 'second-ocr-asset.jpg'; + mockStack = createMockStack(primaryAssetDto, [secondAssetDto], new Set()); + }); + + test.beforeEach(async ({ context }) => { + await setupBrokenAssetMockApiRoutes(context, mockStack); + + const ocrDataByAssetId = new Map([ + [primaryAssetDto.id, createMockOcrData(primaryAssetDto.id, PRIMARY_OCR_BOXES)], + [secondAssetDto.id, createMockOcrData(secondAssetDto.id, SECONDARY_OCR_BOXES)], + ]); + + await setupOcrMockApiRoutes(context, ocrDataByAssetId); + }); + + test('different OCR boxes shown for different stacked assets', async ({ page }) => { + await page.goto(`/photos/${fixture.primaryAsset.id}`); + await assetViewerUtils.waitForViewerLoad(page, fixture.primaryAsset); + + const ocrButton = page.getByLabel('Text recognition'); + await expect(ocrButton).toBeVisible(); + await ocrButton.click(); + + const ocrBoxes = page.locator('[data-viewer-content] [data-testid="ocr-box"]'); + await expect(ocrBoxes).toHaveCount(2); + await expect(ocrBoxes.nth(0)).toContainText('Hello World'); + + const stackThumbnails = page.locator('#stack-slideshow [data-asset]'); + await expect(stackThumbnails).toHaveCount(2); + await stackThumbnails.nth(1).click(); + + // refreshOcr() clears showOverlay when switching assets, so re-enable it + await expect(ocrBoxes).toHaveCount(0); + await expect(ocrButton).toBeVisible(); + await ocrButton.click(); + + await expect(ocrBoxes).toHaveCount(1); + await expect(ocrBoxes.first()).toContainText('Second Asset Text'); + }); +}); + +test.describe('OCR boxes and zoom', () => { + const fixture = setupAssetViewerFixture(922); + + test.beforeEach(async ({ context }) => { + const primaryAssetDto = toAssetResponseDto(fixture.primaryAsset); + const ocrDataByAssetId = new Map([ + [primaryAssetDto.id, createMockOcrData(primaryAssetDto.id, PRIMARY_OCR_BOXES)], + ]); + + await setupOcrMockApiRoutes(context, ocrDataByAssetId); + }); + + test('OCR boxes scale with zoom', async ({ page }) => { + await page.goto(`/photos/${fixture.primaryAsset.id}`); + await assetViewerUtils.waitForViewerLoad(page, fixture.primaryAsset); + + const ocrButton = page.getByLabel('Text recognition'); + await expect(ocrButton).toBeVisible(); + await ocrButton.click(); + + const ocrBox = page.locator('[data-viewer-content] [data-testid="ocr-box"]').first(); + await expect(ocrBox).toBeVisible(); + + const initialBox = await ocrBox.boundingBox(); + expect(initialBox).toBeTruthy(); + + const { width, height } = page.viewportSize()!; + await page.mouse.move(width / 2, height / 2); + await page.mouse.wheel(0, -3); + + await expect(async () => { + const zoomedBox = await ocrBox.boundingBox(); + expect(zoomedBox).toBeTruthy(); + expect(zoomedBox!.width).toBeGreaterThan(initialBox!.width); + expect(zoomedBox!.height).toBeGreaterThan(initialBox!.height); + }).toPass({ timeout: 2000 }); + }); +}); + +test.describe('OCR text interaction', () => { + const fixture = setupAssetViewerFixture(923); + + test.beforeEach(async ({ context }) => { + const primaryAssetDto = toAssetResponseDto(fixture.primaryAsset); + const ocrDataByAssetId = new Map([ + [primaryAssetDto.id, createMockOcrData(primaryAssetDto.id, PRIMARY_OCR_BOXES)], + ]); + + await setupOcrMockApiRoutes(context, ocrDataByAssetId); + }); + + test('OCR text box has data-overlay-interactive attribute', async ({ page }) => { + await page.goto(`/photos/${fixture.primaryAsset.id}`); + await assetViewerUtils.waitForViewerLoad(page, fixture.primaryAsset); + + await page.getByLabel('Text recognition').click(); + + const ocrBox = page.locator('[data-viewer-content] [data-testid="ocr-box"]').first(); + await expect(ocrBox).toBeVisible(); + await expect(ocrBox).toHaveAttribute('data-overlay-interactive'); + }); + + test('OCR text box receives focus on click', async ({ page }) => { + await page.goto(`/photos/${fixture.primaryAsset.id}`); + await assetViewerUtils.waitForViewerLoad(page, fixture.primaryAsset); + + await page.getByLabel('Text recognition').click(); + + const ocrBox = page.locator('[data-viewer-content] [data-testid="ocr-box"]').first(); + await expect(ocrBox).toBeVisible(); + + await ocrBox.click(); + await expect(ocrBox).toBeFocused(); + }); + + test('dragging on OCR text box does not trigger image pan', async ({ page }) => { + await page.goto(`/photos/${fixture.primaryAsset.id}`); + await assetViewerUtils.waitForViewerLoad(page, fixture.primaryAsset); + + await page.getByLabel('Text recognition').click(); + + const ocrBox = page.locator('[data-viewer-content] [data-testid="ocr-box"]').first(); + await expect(ocrBox).toBeVisible(); + + const imgLocator = page.locator('[data-viewer-content] img[draggable="false"]'); + const initialTransform = await imgLocator.evaluate((element) => { + return getComputedStyle(element.closest('[style*="transform"]') ?? element).transform; + }); + + const box = await ocrBox.boundingBox(); + expect(box).toBeTruthy(); + const centerX = box!.x + box!.width / 2; + const centerY = box!.y + box!.height / 2; + + await page.mouse.move(centerX, centerY); + await page.mouse.down(); + await page.mouse.move(centerX + 50, centerY + 30, { steps: 5 }); + await page.mouse.up(); + + const afterTransform = await imgLocator.evaluate((element) => { + return getComputedStyle(element.closest('[style*="transform"]') ?? element).transform; + }); + expect(afterTransform).toBe(initialTransform); + }); + + test('split touch gesture across zoom container does not trigger zoom', async ({ page }) => { + await page.goto(`/photos/${fixture.primaryAsset.id}`); + await assetViewerUtils.waitForViewerLoad(page, fixture.primaryAsset); + + await page.getByLabel('Text recognition').click(); + const ocrBox = page.locator('[data-viewer-content] [data-testid="ocr-box"]').first(); + await expect(ocrBox).toBeVisible(); + + const imgLocator = page.locator('[data-viewer-content] img[draggable="false"]'); + const initialTransform = await imgLocator.evaluate((element) => { + return getComputedStyle(element.closest('[style*="transform"]') ?? element).transform; + }); + + const viewerContent = page.locator('[data-viewer-content]'); + const viewerBox = await viewerContent.boundingBox(); + expect(viewerBox).toBeTruthy(); + + // Dispatch a synthetic split gesture: one touch inside the viewer, one outside + await page.evaluate( + ({ viewerCenterX, viewerCenterY, outsideY }) => { + const viewer = document.querySelector('[data-viewer-content]'); + if (!viewer) { + return; + } + + const createTouch = (id: number, x: number, y: number) => { + return new Touch({ + identifier: id, + target: viewer, + clientX: x, + clientY: y, + }); + }; + + const insideTouch = createTouch(0, viewerCenterX, viewerCenterY); + const outsideTouch = createTouch(1, viewerCenterX, outsideY); + + const touchStartEvent = new TouchEvent('touchstart', { + touches: [insideTouch, outsideTouch], + targetTouches: [insideTouch], + changedTouches: [insideTouch, outsideTouch], + bubbles: true, + cancelable: true, + }); + + const touchMoveEvent = new TouchEvent('touchmove', { + touches: [createTouch(0, viewerCenterX, viewerCenterY - 30), createTouch(1, viewerCenterX, outsideY + 30)], + targetTouches: [createTouch(0, viewerCenterX, viewerCenterY - 30)], + changedTouches: [ + createTouch(0, viewerCenterX, viewerCenterY - 30), + createTouch(1, viewerCenterX, outsideY + 30), + ], + bubbles: true, + cancelable: true, + }); + + const touchEndEvent = new TouchEvent('touchend', { + touches: [], + targetTouches: [], + changedTouches: [insideTouch, outsideTouch], + bubbles: true, + cancelable: true, + }); + + viewer.dispatchEvent(touchStartEvent); + viewer.dispatchEvent(touchMoveEvent); + viewer.dispatchEvent(touchEndEvent); + }, + { + viewerCenterX: viewerBox!.x + viewerBox!.width / 2, + viewerCenterY: viewerBox!.y + viewerBox!.height / 2, + outsideY: 10, // near the top of the page, outside the viewer + }, + ); + + const afterTransform = await imgLocator.evaluate((element) => { + return getComputedStyle(element.closest('[style*="transform"]') ?? element).transform; + }); + expect(afterTransform).toBe(initialTransform); + }); +}); diff --git a/e2e/src/ui/specs/search/search-gallery.e2e-spec.ts b/e2e/src/ui/specs/search/search-gallery.e2e-spec.ts index c3721b1c54..87f809de75 100644 --- a/e2e/src/ui/specs/search/search-gallery.e2e-spec.ts +++ b/e2e/src/ui/specs/search/search-gallery.e2e-spec.ts @@ -6,6 +6,7 @@ import { generateTimelineData, TimelineAssetConfig, TimelineData, + toAssetResponseDto, } from 'src/ui/generators/timeline'; import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network'; import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network'; @@ -30,6 +31,10 @@ test.describe('search gallery-viewer', () => { }; test.beforeAll(async () => { + test.fail( + process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS !== '1', + 'This test requires env var: PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1', + ); adminUserId = faker.string.uuid(); testContext.adminId = adminUserId; timelineRestData = generateTimelineData({ ...createDefaultTimelineConfig(), ownerId: adminUserId }); @@ -44,7 +49,10 @@ test.describe('search gallery-viewer', () => { await context.route('**/api/search/metadata', async (route, request) => { if (request.method() === 'POST') { - const searchAssets = assets.slice(0, 5).filter((asset) => !changes.assetDeletions.includes(asset.id)); + const searchAssets = assets + .slice(0, 5) + .filter((asset) => !changes.assetDeletions.includes(asset.id)) + .map((asset) => toAssetResponseDto(asset)); return route.fulfill({ status: 200, contentType: 'application/json', diff --git a/e2e/src/ui/specs/timeline/utils.ts b/e2e/src/ui/specs/timeline/utils.ts index b7003295cf..e67229d3c9 100644 --- a/e2e/src/ui/specs/timeline/utils.ts +++ b/e2e/src/ui/specs/timeline/utils.ts @@ -62,7 +62,7 @@ export const thumbnailUtils = { return page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"]`); }, selectButton(page: Page, assetId: string) { - return page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"] button`); + return page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"] button[role="checkbox"]`); }, selectedAsset(page: Page) { return page.locator('[data-thumbnail-focus-container][data-selected]'); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index a5567f0778..aa4c3b8499 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -3,7 +3,6 @@ import { AssetMediaResponseDto, AssetResponseDto, AssetVisibility, - CheckExistingAssetsDto, CreateAlbumDto, CreateLibraryDto, JobCreateDto, @@ -20,7 +19,6 @@ import { UserAdminCreateDto, UserPreferencesUpdateDto, ValidateLibraryDto, - checkExistingAssets, createAlbum, createApiKey, createJob, @@ -343,8 +341,6 @@ export const utils = { }, ) => { const _dto = { - deviceAssetId: 'test-1', - deviceId: 'test', fileCreatedAt: new Date().toISOString(), fileModifiedAt: new Date().toISOString(), ...dto, @@ -375,40 +371,6 @@ export const utils = { return body as AssetMediaResponseDto; }, - replaceAsset: async ( - accessToken: string, - assetId: string, - dto?: Partial> & { assetData?: FileData }, - ) => { - const _dto = { - deviceAssetId: 'test-1', - deviceId: 'test', - fileCreatedAt: new Date().toISOString(), - fileModifiedAt: new Date().toISOString(), - ...dto, - }; - - const assetData = dto?.assetData?.bytes || makeRandomImage(); - const filename = dto?.assetData?.filename || 'example.png'; - - if (dto?.assetData?.bytes) { - console.log(`Uploading ${filename}`); - } - - const builder = request(app) - .put(`/assets/${assetId}/original`) - .attach('assetData', assetData, filename) - .set('Authorization', `Bearer ${accessToken}`); - - for (const [key, value] of Object.entries(_dto)) { - void builder.field(key, String(value)); - } - - const { body } = await builder; - - return body as AssetMediaResponseDto; - }, - createImageFile: (path: string) => { if (!existsSync(dirname(path))) { mkdirSync(dirname(path), { recursive: true }); @@ -450,9 +412,6 @@ export const utils = { getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), - checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) => - checkExistingAssets({ checkExistingAssetsDto }, { headers: asBearerAuth(accessToken) }), - searchAssets: async (accessToken: string, dto: MetadataSearchDto) => { return searchAssets({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) }); }, @@ -510,6 +469,9 @@ export const utils = { createStack: (accessToken: string, assetIds: string[]) => createStack({ stackCreateDto: { assetIds } }, { headers: asBearerAuth(accessToken) }), + setAssetDuplicateId: (accessToken: string, assetId: string, duplicateId: string | null) => + updateAssets({ assetBulkUpdateDto: { ids: [assetId], duplicateId } }, { headers: asBearerAuth(accessToken) }), + upsertTags: (accessToken: string, tags: string[]) => upsertTags({ tagUpsertDto: { tags } }, { headers: asBearerAuth(accessToken) }), diff --git a/e2e/test-assets b/e2e/test-assets index 163c251744..0eac5a3738 160000 --- a/e2e/test-assets +++ b/e2e/test-assets @@ -1 +1 @@ -Subproject commit 163c251744e0a35d7ecfd02682452043f149fc2b +Subproject commit 0eac5a37384c151be88381b41f9e28d8d59a4466 diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json index f6efbf41e9..61eefdac07 100644 --- a/e2e/tsconfig.json +++ b/e2e/tsconfig.json @@ -14,8 +14,10 @@ "outDir": "./dist", "incremental": true, "skipLibCheck": true, + "paths": { + "src/*": ["./src/*"] + }, "esModuleInterop": true, - "baseUrl": "./" }, "include": ["src/**/*.ts", "vitest*.config.ts"], "exclude": ["dist", "node_modules"] diff --git a/i18n/af.json b/i18n/af.json index e641c07b21..95919bb1dc 100644 --- a/i18n/af.json +++ b/i18n/af.json @@ -178,6 +178,17 @@ "stop_motion_photo": "Stop bewegingsfoto", "stop_photo_sharing": "Staak die deel van u foto’s?", "stop_photo_sharing_description": "{partner} sal nie meer toegang tot u foto’s hÃĒ nie.", + "unnamed_share": "Naamlose deelskakel", + "unsaved_change": "Onbewaarde verandering", + "unselect_all": "Ontkies alles", + "unselect_all_duplicates": "Ontkies alle duplikate", + "unselect_all_in": "Ontkies alles in {group}", + "unstack": "Ontstapel", + "unstack_action_prompt": "{count} ongestapel", + "unstacked_assets_count": "{count, plural, one {# item} other {# items}} ontstapel", + "unsupported_field_type": "Onondersteunde veldtipe", + "unsupported_file_type": "LÃĒer {file} kan nie opgelaai word nie omdat die lÃĒertipe {type} nie ondersteun word nie.", + "untagged": "Sonder etiket", "untitled_workflow": "Naamlose werkvloei", "up_next": "Volgende", "update_location_action_prompt": "Werk die ligging van {count} gekose items by met:", @@ -187,6 +198,7 @@ "upload_concurrency": "Aantal gelyktydige oplaaie", "upload_details": "Oplaaidetails", "upload_dialog_info": "Wil u ’n rugsteun maak van die gekose item(s) op die bediener?", + "upload_dialog_title": "Laai item op", "upload_error_with_count": "Oplaaifout vir {count, plural, one {# item} other {# items}}", "upload_errors": "Oplaai voltooi met {count, plural, one {# fout} other {# foute}}, verfris die blad om die nuwe items te sien.", "upload_finished": "Klaar opgelaai", @@ -257,6 +269,7 @@ "viewer_remove_from_stack": "Verwyder van stapel", "viewer_stack_use_as_main_asset": "Gebruik as hoofitem", "viewer_unstack": "Ontstapel", + "visibility": "Sigbaarheid", "visibility_changed": "Sigbaarheid verander vir {count, plural, one {# mens} other {# mense}}", "visual": "Visueel", "visual_builder": "Visuele bouer", diff --git a/i18n/ar.json b/i18n/ar.json index 3834d76a5f..fe0b9d072c 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -3,7 +3,7 @@ "account": "Ø­ØŗØ§Ø¨", "account_settings": "ØĨؚداداØĒ Ø§Ų„Ø­ØŗØ§Ø¨", "acknowledge": "ØŖŲØ¯ØąŲƒ Ø°Ų„Ųƒ", - "action": "ØšŲ…Ų„ŲŠØŠ", + "action": "ØĨØŦØąØ§ØĄ", "action_common_update": "ØĒØ­Ø¯ŲŠØĢ", "action_description": "Ų…ØŦŲ…ŲˆØšØŠ Ų…Ų† Ø§Ų„ŲØšØ§Ų„ŲŠØ§ØĒ Ø§Ų„ØĒ؊ ØŗØĒŲ†ŲØ° ØšŲ„Ų‰ Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØĒ؊ ØĒŲ… ØĒØĩ؁؊ØĒŲ‡Ø§", "actions": "ØšŲ…Ų„ŲŠØ§ØĒ", @@ -61,8 +61,8 @@ "backup_onboarding_1_description": "Ų†ØŗØŽØŠ ØŽØ§ØąØŦ Ø§Ų„Ų…ŲˆŲ‚Øš ؁؊ Ų…ŲˆŲ‚Øš ØĸØŽØą.", "backup_onboarding_2_description": "Ų†ØŗØŽ Ų…Ø­Ų„ŲŠØŠ ØšŲ„Ų‰ ØŖØŦŲ‡Ø˛ØŠ Ų…ØŽØĒŲ„ŲØŠ. ŲŠØ´Ų…Ų„ Ø°Ų„Ųƒ Ø§Ų„Ų…Ų„ŲØ§ØĒ Ø§Ų„ØąØĻŲŠØŗŲŠØŠ ŲˆŲ†ØŗØŽØŠ احØĒŲŠØ§ØˇŲŠØŠ Ų…Ø­Ų„ŲŠØŠ Ų…Ų†Ų‡Ø§.", "backup_onboarding_3_description": "ØĨØŦŲ…Ø§Ų„ŲŠ Ų†ŲØŗØŽ Ø¨ŲŠØ§Ų†Ø§ØĒŲƒØŒ Ø¨Ų…Ø§ ؁؊ Ø°Ų„Ųƒ Ø§Ų„Ų…Ų„ŲØ§ØĒ Ø§Ų„ØŖØĩŲ„ŲŠØŠ. ŲŠØ´Ų…Ų„ Ø°Ų„Ųƒ Ų†ØŗØŽØŠŲ‹ ŲˆØ§Ø­Ø¯ØŠŲ‹ ØŽØ§ØąØŦ Ø§Ų„Ų…ŲˆŲ‚Øš ŲˆŲ†ØŗØŽØĒŲŠŲ† Ų…Ø­Ų„ŲŠØĒŲŠŲ†.", - "backup_onboarding_description": "ŲŠŲŲ†ØĩØ­ باØĒباؚ Ø§ØŗØĒØąØ§ØĒ؊ØŦŲŠØŠ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ 3-2-1 Ų„Ø­Ų…Ø§ŲŠØŠ Ø¨ŲŠØ§Ų†Ø§ØĒ؃. احØĒŲØ¸ Ø¨Ų†ØŗØŽ احØĒŲŠØ§ØˇŲŠØŠ Ų…Ų† ØĩŲˆØąŲƒ/ŲŲŠØ¯ŲŠŲˆŲ‡Ø§ØĒ؃ Ø§Ų„Ų…Ø­Ų…Ų‘Ų„ØŠØŒ Ø¨Ø§Ų„ØĨØļØ§ŲØŠ ØĨŲ„Ų‰ Ų‚Ø§ØšØ¯ØŠ Ø¨ŲŠØ§Ų†Ø§ØĒ Immich، Ų„ØļŲ…Ø§Ų† Ø­Ų„ Ų†ØŗØŽ احØĒŲŠØ§ØˇŲŠ Ø´Ø§Ų…Ų„.", - "backup_onboarding_footer": "Ų„Ų…Ø˛ŲŠØ¯ Ų…Ų† Ø§Ų„Ų…ØšŲ„ŲˆŲ…Ø§ØĒ Ø­ŲˆŲ„ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų€ Immich، ŲŠØąØŦŲ‰ Ø§Ų„ØąØŦŲˆØš ØĨŲ„Ų‰ Ø§Ų„ØĒØšŲ„ŲŠŲ…Ø§ØĒ .", + "backup_onboarding_description": "ŲŠŲŲ†ØĩØ­ باØĒباؚ Ø§ØŗØĒØąØ§ØĒ؊ØŦŲŠØŠ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ 3-2- 1 Ų„Ø­Ų…Ø§ŲŠØŠ Ø¨ŲŠØ§Ų†Ø§ØĒ؃. احØĒŲØ¸ Ø¨Ų†ØŗØŽ احØĒŲŠØ§ØˇŲŠØŠ Ų…Ų† ØĩŲˆØąŲƒ/ŲŲŠØ¯ŲŠŲˆŲ‡Ø§ØĒ؃ Ø§Ų„Ų…Ø­Ų…Ų‘Ų„ØŠØŒ Ø¨Ø§Ų„ØĨØļØ§ŲØŠ ØĨŲ„Ų‰ Ų‚Ø§ØšØ¯ØŠ Ø¨ŲŠØ§Ų†Ø§ØĒ Immich، Ų„ØļŲ…Ø§Ų† Ø­Ų„ Ų†ØŗØŽ احØĒŲŠØ§ØˇŲŠ Ø´Ø§Ų…Ų„.", + "backup_onboarding_footer": "Ų„Ų…Ø˛ŲŠØ¯ Ų…Ų† Ø§Ų„Ų…ØšŲ„ŲˆŲ…Ø§ØĒ Ø­ŲˆŲ„ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų€ Immich، ŲŠØąØŦŲ‰ Ø§Ų„ØąØŦŲˆØš ØĨŲ„Ų‰ Ø§Ų„ŲˆØĢاØĻŲ‚.", "backup_onboarding_parts_title": "؊ØĒØļŲ…Ų† Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ 3-2-1 Ų…Ø§ ŲŠŲ„ŲŠ:", "backup_onboarding_title": "Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠØŠ", "backup_settings": "ØĨؚداداØĒ ØĒŲØąŲŠØē Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ", @@ -333,7 +333,7 @@ "storage_template_migration_description": "Ų‚Ų… بØĒØˇØ¨ŲŠŲ‚ Ø§Ų„Ų‚Ø§Ų„Ø¨ Ø§Ų„Ø­Ø§Ų„ŲŠ {template} ØšŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„ØĒ؊ ØĒŲ… ØąŲØšŲ‡Ø§ ØŗØ§Ø¨Ų‚Ų‹Ø§", "storage_template_migration_info": "ØĒØēŲŠŲŠØąØ§ØĒ Ø§Ų„Ų†Ų…ŲˆØ°ØŦ Ø§Ų„ØŽØ˛Ų†ŲŠ ØŗØĒØēŲŠØą ØŦŲ…ŲŠØš Ø§Ų„Øĩ؊Øē Ø§Ų„Ų‰ Ø§Ø­ØąŲ ØĩØēŲŠØąØŠ. ØĒØēŲŠŲŠØąØ§ØĒ Ø§Ų„Ų†Ų…ŲˆØ°ØŦ ØŗØĒŲ†ØˇØ¨Ų‚ ŲŲ‚Øˇ ØšŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„ØŦØ¯ŲŠØ¯ØŠ. Ų„ØĒØˇØ¨ŲŠŲ‚ Ø§Ų„Ų†Ų…ŲˆØ°ØŦ ØšŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„ØĒ؊ ØĒŲ… ØąŲØšŲ‡Ø§ ØŗØ§Ø¨Ų‚Ų‹Ø§ØŒ Ų‚Ų… بØĒØ´ØēŲŠŲ„ {job}.", "storage_template_migration_job": "ŲˆØ¸ŲŠŲØŠ ØĒŲ‡ØŦŲŠØą Ų‚Ø§Ų„Ø¨ Ø§Ų„ØĒØŽØ˛ŲŠŲ†", - "storage_template_more_details": "Ų„Ų…Ø˛ŲŠØ¯ Ų…Ų† Ø§Ų„ØĒŲØ§ØĩŲŠŲ„ Ø­ŲˆŲ„ Ų‡Ø°Ų‡ Ø§Ų„Ų…ŲŠØ˛ØŠØŒ ŲŠØąØŦŲ‰ Ø§Ų„ØąØŦŲˆØš ØĨŲ„Ų‰ Storage Template ؈implications", + "storage_template_more_details": "Ų„Ų…Ø˛ŲŠØ¯ Ų…Ų† Ø§Ų„ØĒŲØ§ØĩŲŠŲ„ Ø­ŲˆŲ„ Ų‡Ø°Ų‡ Ø§Ų„Ų…ŲŠØ˛ØŠØŒ ŲŠØąØŦŲ‰ Ø§Ų„ØąØŦŲˆØš ØĨŲ„Ų‰ Storage Template ؈ implications.", "storage_template_onboarding_description_v2": "ØšŲ†Ø¯ Ø§Ų„ØĒŲØšŲŠŲ„. Ų‡Ø°Ų‡ Ø§Ų„ØŽØ§ØĩŲŠØŠ ØŗØĒŲ‚ŲˆŲ… Ø¨Ø§Ų„ØĒØąØĒŲŠØ¨ Ø§Ų„ØĒŲ„Ų‚Ø§ØĻ؊ Ų„Ų„Ų…Ų„ŲØ§ØĒ Ø¨Ų†Ø§ØĄ ØšŲ„Ų‰ Ų†Ų…ŲˆØ°ØŦ Ų…ØšØąŲ Ų…Ų† Ų‚Ø¨Ų„ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…. ØąØŦØ§ØĄ Ø§ØˇŲ„Øš ØšŲ„Ų‰ Ø§Ų„ØĒ؈ØĢŲŠŲ‚.", "storage_template_path_length": "Ø§Ų„Ø­Ø¯ Ø§Ų„ØĒŲ‚ØąŲŠØ¨ŲŠ Ų„ØˇŲˆŲ„ Ø§Ų„Ų…ØŗØ§Øą: {length, number}/{limit, number}", "storage_template_settings": "Ų‚Ø§Ų„Ø¨ Ø§Ų„ØĒØŽØ˛ŲŠŲ†", @@ -372,7 +372,7 @@ "transcoding_audio_codec": "ŲƒŲˆØ¯ Ø§Ų„Øĩ؈ØĒ", "transcoding_audio_codec_description": "Opus Ų‡Ųˆ Ø§Ų„ØŽŲŠØ§Øą Ø°Ųˆ ØŖØšŲ„Ų‰ ØŦŲˆØ¯ØŠØŒ ŲˆŲ„ŲƒŲ†Ų‡ ؊ØĒŲ…ØĒØš بØĒŲˆØ§ŲŲ‚ ØŖŲ‚Ų„ Ų…Øš Ø§Ų„ØŖØŦŲ‡Ø˛ØŠ ØŖŲˆ Ø§Ų„Ø¨ØąŲ…ØŦŲŠØ§ØĒ Ø§Ų„Ų‚Ø¯ŲŠŲ…ØŠ.", "transcoding_bitrate_description": "Ų…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„ØĒ؊ ؊ØĒØŦØ§ŲˆØ˛ Ų…ØšØ¯Ų„ Ø§Ų„Ø¨ØĒ ØŖŲ‚ØĩŲ‰ Ų‚ŲŠŲ…ØŠ ØŖŲˆ Ø§Ų„ØĒ؊ Ų„Ø§ ØĒŲƒŲˆŲ† ؁؊ ØĒŲ†ØŗŲŠŲ‚ Ų…Ų‚Ø¨ŲˆŲ„", - "transcoding_codecs_learn_more": "Ų„Ų…ØšØąŲØŠ Ø§Ų„Ų…Ø˛ŲŠØ¯ Ø­ŲˆŲ„ Ø§Ų„Ų…ØĩØˇŲ„Ø­Ø§ØĒ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ØŠ Ų‡Ų†Ø§ØŒ ŲŠØąØŦŲ‰ Ø§Ų„ØąØŦŲˆØš ØĨŲ„Ų‰ ؈ØĢاØĻŲ‚ FFmpeg Ų„Ų„H.264 codec, HEVC codec and VP9 codec.", + "transcoding_codecs_learn_more": "Ų„Ų…ØšØąŲØŠ Ø§Ų„Ų…Ø˛ŲŠØ¯ Ø­ŲˆŲ„ Ø§Ų„Ų…ØĩØˇŲ„Ø­Ø§ØĒ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ØŠ Ų‡Ų†Ø§ØŒ ŲŠØąØŦŲ‰ Ø§Ų„ØąØŦŲˆØš ØĨŲ„Ų‰ ؈ØĢاØĻŲ‚ FFmpeg Ų„Ų€ H.264 codec، ؈ HEVC codec ؈ VP9 codec.", "transcoding_constant_quality_mode": "؈ØļØš Ø§Ų„ØŦŲˆØ¯ØŠ Ø§Ų„ØĢابØĒØŠ", "transcoding_constant_quality_mode_description": "ICQ ØŖŲØļŲ„ Ų…Ų† CQP، ŲˆŲ„ŲƒŲ† بؚØļ ØŖØŦŲ‡Ø˛ØŠ ØšØĒاد Ø§Ų„ØĒØŗØąŲŠØš Ų„Ø§ ØĒØ¯ØšŲ… Ų‡Ø°Ø§ Ø§Ų„ŲˆØļØš. ØĒØšŲŠŲŠŲ† Ų‡Ø°Ø§ Ø§Ų„ØŽŲŠØ§Øą ŲŠØŗØŦØšŲ„ Ø§Ų„ØŖŲØļŲ„ŲŠØŠ Ų„Ų„ŲˆØļØš Ø§Ų„Ų…Ø­Ø¯Ø¯ ØšŲ†Ø¯ Ø§ØŗØĒØŽØ¯Ø§Ų… Ø§Ų„ØĒØąŲ…ŲŠØ˛ Ø¨Ų†Ø§ØĄŲ‹ ØšŲ„Ų‰ Ø§Ų„ØŦŲˆØ¯ØŠ. ؊ØĒŲ… ØĒØŦØ§Ų‡Ų„Ų‡ Ø¨ŲˆØ§ØŗØˇØŠ NVENC Ų„ØŖŲ†Ų‡ Ų„Ø§ ŲŠØ¯ØšŲ… ICQ.", "transcoding_constant_rate_factor": "ØšØ§Ų…Ų„ Ų…ØšØ¯Ų„ Ø§Ų„ØŦŲˆØ¯ØŠ Ø§Ų„ØĢابØĒ (-crf)", @@ -441,7 +441,7 @@ "user_successfully_removed": "Ø§Ų„Ų…ØŗØĒØŽØ¯Ų… {email} ØĒŲ…ØĒ Ø§Ø˛Ø§Ų„ØĒŲ‡ Ø¨Ų†ØŦاح.", "users_page_description": "ØĩŲØ­ØŠ Ø§Ø¯Ø§ØąØŠ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ†", "version_check_enabled_description": "ØĒŲØšŲŠŲ„ Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† Ø§Ų„ØĨØĩØ¯Ø§ØąØ§ØĒ Ø§Ų„ØŦØ¯ŲŠØ¯ØŠ", - "version_check_implications": "ØĒØšØĒŲ…Ø¯ Ų…ŲŠØ˛ØŠ Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† Ø§Ų„ØĨØĩØ¯Ø§Øą ØšŲ„Ų‰ Ø§Ų„ØĒŲˆØ§ØĩŲ„ Ø§Ų„Ø¯ŲˆØąŲŠ Ų…Øš github.com", + "version_check_implications": "ØĒØšØĒŲ…Ø¯ Ų…ŲŠØ˛ØŠ Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† Ø§Ų„ØĨØĩØ¯Ø§Øą ØšŲ„Ų‰ Ø§Ų„ØĒŲˆØ§ØĩŲ„ Ø§Ų„Ø¯ŲˆØąŲŠ Ų…Øš {server}", "version_check_settings": "Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† Ø§Ų„ØĨØĩØ¯Ø§Øą", "version_check_settings_description": "ØĒŲØšŲŠŲ„/ØĒØšØˇŲŠŲ„ Ø§Ų„ØĨØ´ØšØ§Øą Ų„ØĨØĩØ¯Ø§Øą ØŦØ¯ŲŠØ¯", "video_conversion_job": "ØĒØ­ŲˆŲŠŲ„ ØŖØ´ØąØˇØŠ Ø§Ų„ŲŲŠØ¯ŲŠŲˆ", @@ -849,9 +849,12 @@ "create_link_to_share": "ØĨŲ†Ø´Ø§ØĄ ØąØ§Ø¨Øˇ Ų„Ų„Ų…Ø´Ø§ØąŲƒØŠ", "create_link_to_share_description": "Ø§Ų„ØŗŲ…Ø§Ø­ Ų„ØŖŲŠ Ø´ØŽØĩ Ų„Ø¯ŲŠŲ‡ Ø§Ų„ØąØ§Ø¨Øˇ Ø¨Ų…Ø´Ø§Ų‡Ø¯ØŠ Ø§Ų„ØĩŲˆØąØŠ (Ø§Ų„ØĩŲˆØą) Ø§Ų„Ų…Ø­Ø¯Ø¯ØŠ", "create_new": "Ø§Ų†Ø´Ø§ØĄ ØŦØ¯ŲŠØ¯", + "create_new_face": "ØĨŲ†Ø´Ø§ØĄ ؈ØŦŲ‡ ØŦØ¯ŲŠØ¯", "create_new_person": "ØĨŲ†Ø´Ø§ØĄ Ø´ØŽØĩ ØŦØ¯ŲŠØ¯", "create_new_person_hint": "ØĒØšŲŠŲŠŲ† Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„Ų…Ø­Ø¯Ø¯ØŠ Ų„Ø´ØŽØĩ ØŦØ¯ŲŠØ¯", "create_new_user": "ØĨŲ†Ø´Ø§ØĄ Ų…ØŗØĒØŽØ¯Ų… ØŦØ¯ŲŠØ¯", + "create_person": "ØĨŲ†Ø´Ø§ØĄ Ø´ØŽØĩ", + "create_person_subtitle": "ØŖØļ؁ Ø§ØŗŲ…Ø§Ų‹ Ų„Ų„ŲˆØŦŲ‡ Ø§Ų„Ų…Ø­Ø¯Ø¯ Ų„ØĨŲ†Ø´Ø§ØĄ Ø§Ų„Ø´ØŽØĩ Ø§Ų„ØŦØ¯ŲŠØ¯ ŲˆØ§Ų„ØĨØ´Ø§ØąØŠ ØĨŲ„ŲŠŲ‡", "create_shared_album_page_share_add_assets": "ØĨØļØ§ŲØŠ Ø§Ų„ØŖØĩŲˆŲ„", "create_shared_album_page_share_select_photos": "حدد Ø§Ų„ØĩŲˆØą", "create_shared_link": "Ø§Ų†Ø´Ø§ØĄ ØąØ§Ø¨Øˇ Ų…Ø´ØĒØąŲƒ", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "ØĒŲ… Ø§Ų„Ø§ØĩŲ„Ø§Ø­", "crop_aspect_ratio_free": "Ø­Øą", "crop_aspect_ratio_original": "اØĩŲ„ŲŠ", + "crop_aspect_ratio_square": "Ų…ØąØ¨Øš", "curated_object_page_title": "ØŖØ´ŲŠØ§ØĄ", "current_device": "Ø§Ų„ØŦŲ‡Ø§Ø˛ Ø§Ų„Ø­Ø§Ų„ŲŠ", "current_pin_code": "ØąŲ…Ø˛ PIN Ø§Ų„Ø­Ø§Ų„ŲŠ", @@ -880,7 +884,7 @@ "daily_title_text_date": "E ، MMM DD", "daily_title_text_date_year": "E ، MMM DD ، yyyy", "dark": "Ų…ØšØĒŲ…", - "dark_theme": "ØĒØ¨Ø¯ŲŠŲ„ Ø§Ų„Ų…Ø¸Ų‡Øą Ø§Ų„Ø¯Ø§ŲƒŲ†", + "dark_theme": "ØĒØ¨Ø¯ŲŠŲ„ Ø§Ų„Ų…Ø¸Ų‡Øą ØĨŲ„Ų‰ Ø§Ų„Ø¯Ø§ŲƒŲ†", "date": "ØĒØ§ØąŲŠØŽ", "date_after": "Ø§Ų„ØĒØ§ØąØŽ بؚد", "date_and_time": "Ø§Ų„ØĒØ§ØąŲŠØŽ ؈ Ø§Ų„ŲˆŲ‚ØĒ", @@ -891,10 +895,8 @@ "day": "ŲŠŲˆŲ…", "days": "Ø§ŲŠØ§Ų…", "deduplicate_all": "ØĨŲ„ØēØ§ØĄ ØĒŲƒØąØ§Øą Ø§Ų„ŲƒŲ„", - "deduplication_criteria_1": "Ø­ØŦŲ… Ø§Ų„ØĩŲˆØąØŠ Ø¨ŲˆØ­Ø¯Ø§ØĒ Ø§Ų„Ø¨Ø§ŲŠØĒ", - "deduplication_criteria_2": "ؚدد Ø¨ŲŠØ§Ų†Ø§ØĒ EXIF", - "deduplication_info": "Ų…ØšŲ„ŲˆŲ…Ø§ØĒ ØĨŲ„ØēØ§ØĄ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ Ø§Ų„Ų…ŲƒØąØąØŠ", - "deduplication_info_description": "Ų„ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ØŖØĩŲˆŲ„ Ų…ØŗØ¨Ų‚Ø§ ØĒŲ„Ų‚Ø§ØĻŲŠØ§ ؈ØĨØ˛Ø§Ų„ØŠ Ø§Ų„ØĒŲƒØąØ§ØąØ§ØĒ Ø¨ŲƒŲ…ŲŠØ§ØĒ ŲƒØ¨ŲŠØąØŠØŒ Ų†Ų†Ø¸Øą ØĨŲ„Ų‰:", + "default_locale": "Ø§Ų„ØĨؚداداØĒ Ø§Ų„Ų…Ø­Ų„ŲŠØŠ Ø§Ų„Ø§ŲØĒØąØ§ØļŲŠØŠ", + "default_locale_description": "ØĒŲ†ØŗŲŠŲ‚ Ø§Ų„ØĒŲˆØ§ØąŲŠØŽ ŲˆØ§Ų„ØŖØąŲ‚Ø§Ų… Ø¨Ų†Ø§ØĄŲ‹ ØšŲ„Ų‰ Ø§Ų„ØĨؚداداØĒ Ø§Ų„Ų…Ø­Ų„ŲŠØŠ Ų„Ų„Ų…ØĒØĩŲØ­", "delete": "Ø­Ø°Ų", "delete_action_confirmation_message": "Ų‡Ų„ Ø§Ų†ØĒ Ų…ØĒØŖŲƒØ¯ Ų…Ų† Ø­Ø°Ų Ų‡Ø°Ø§ Ø§Ų„Ų…Ų„ŲØŸ Ų‡Ø°Ø§ ØŗØ¤Ø¯ŲŠ Ø§Ų„Ų‰ Ų†Ų‚Ų„ Ø§Ų„Ų…Ų„Ų Ø§Ų„Ų‰ ØŗŲ„ØŠ Ų…Ų‡Ų…Ų„Ø§ØĒ Ø§Ų„ØŽØ§Ø¯Ų… ŲˆØŗŲŠØĒŲ… Ø§Ø´ØšØ§ØąŲƒ Ø§Ų† ŲƒŲ†ØĒ ØĒØąŲŠØ¯ Ø­Ø°ŲŲ‡ ØšŲ„Ų‰ Ø§Ų„ØŦŲ‡Ø§Ø˛", "delete_action_prompt": "ØĒŲ… Ø­Ø°Ų {count}", @@ -970,7 +972,7 @@ "downloading_media": "ØĒŲ†Ø˛ŲŠŲ„ Ø§Ų„ŲˆØŗØ§ØĻØˇ", "drop_files_to_upload": "Ų‚Ų… بØĨØŗŲ‚Ø§Øˇ Ø§Ų„Ų…Ų„ŲØ§ØĒ ؁؊ ØŖŲŠ Ų…ŲƒØ§Ų† Ų„ØąŲØšŲ‡Ø§", "duplicates": "Ø§Ų„ØĒŲƒØąØ§ØąØ§ØĒ", - "duplicates_description": "Ų‚Ų… Ø¨Ø­Ų„ ŲƒŲ„ Ų…ØŦŲ…ŲˆØšØŠ Ų…Ų† ØŽŲ„Ø§Ų„ Ø§Ų„ØĨØ´Ø§ØąØŠ ØĨŲ„Ų‰ Ø§Ų„ØĒŲƒØąØ§ØąØ§ØĒ، ØĨŲ† ؈ØŦدØĒ", + "duplicates_description": "Ų‚Ų… Ø¨Ø­Ų„ ŲƒŲ„ Ų…ØŦŲ…ŲˆØšØŠ Ų…Ų† ØŽŲ„Ø§Ų„ Ø§Ų„ØĨØ´Ø§ØąØŠ ØĨŲ„Ų‰ Ø§Ų„ØĒŲƒØąØ§ØąØ§ØĒ، ØĨŲ† ؈ØŦدØĒ.", "duration": "Ø§Ų„Ų…Ø¯ØŠ", "edit": "ØĒØšØ¯ŲŠŲ„", "edit_album": "ØĒØšØ¯ŲŠŲ„ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "ØšŲ†ŲˆØ§Ų† Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", "licenses": "ØąŲØŽŲŽØĩ", "light": "Ø§Ų„Ų…Øļ؊ØĻ", + "light_theme": "Ø§Ų„ØĒØ¨Ø¯ŲŠŲ„ ØĨŲ„Ų‰ Ø§Ų„Ų…Ø¸Ų‡Øą Ø§Ų„ŲØ§ØĒØ­", "like": "اؚØŦاب", "like_deleted": "ØĒŲ… Ø­Ø°Ų Ø§Ų„ØĨØšØŦاب", "link_motion_video": "ØąØ§Ø¨Øˇ ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ø­ØąŲƒØŠ", + "link_to_docs": "Ų„Ų…Ø˛ŲŠØ¯ Ų…Ų† Ø§Ų„Ų…ØšŲ„ŲˆŲ…Ø§ØĒ، ŲŠŲØąØŦŲ‰ Ø§Ų„ØąØŦŲˆØš ØĨŲ„Ų‰ Ø§Ų„ŲˆØĢاØĻŲ‚.", "link_to_oauth": "Ø§Ų„ØąØ¨Øˇ Ų…Øš OAuth", "linked_oauth_account": "Ø­ØŗØ§Ø¨ Ų…ØąØĒØ¨Øˇ Ø¨Ų€ OAuth", "list": "Ų‚Ø§ØĻŲ…ØŠ", @@ -1651,6 +1655,7 @@ "only_favorites": "Ø§Ų„Ų…ŲØļŲ„ØŠ ŲŲ‚Øˇ", "open": "؁ØĒØ­", "open_calendar": "Ø§ŲØĒØ­ Ø§Ų„ØąØ˛Ų†Ø§Ų…ØŠ", + "open_in_browser": "؁ØĒØ­ ؁؊ Ų…ØĒØĩŲØ­", "open_in_map_view": "؁ØĒØ­ ؁؊ ØšØąØļ Ø§Ų„ØŽØąŲŠØˇØŠ", "open_in_openstreetmap": "؁ØĒØ­ ؁؊ OpenStreetMap", "open_the_search_filters": "Ø§ŲØĒØ­ Ų…ØąØ´Ø­Ø§ØĒ Ø§Ų„Ø¨Ø­ØĢ", @@ -2212,6 +2217,7 @@ "tag": "Ø§Ų„ØšŲ„Ø§Ų…ØŠ", "tag_assets": "ØŖØĩŲˆŲ„ Ø§Ų„ØšŲ„Ø§Ų…ØŠ", "tag_created": "ØĒŲ… ØĨŲ†Ø´Ø§ØĄ Ø§Ų„ØšŲ„Ø§Ų…ØŠ: {tag}", + "tag_face": "ØšŲ„Ų‘ŲŲ… Ø§Ų„ŲˆØŦŲ‡", "tag_feature_description": "ØĒØĩŲØ­ Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…ØŦŲ…ØšØŠ Ø­ØŗØ¨ Ų…ŲˆØ§ØļŲŠØš Ø§Ų„ØšŲ„Ø§Ų…Ø§ØĒ Ø§Ų„Ų…Ų†ØˇŲ‚ŲŠØŠ", "tag_not_found_question": "Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø§Ų„ØšØĢŲˆØą ØšŲ„Ų‰ ØšŲ„Ø§Ų…ØŠØŸ Ų‚Ų… بØĨŲ†Ø´Ø§ØĄ ØšŲ„Ø§Ų…ØŠ ØŦØ¯ŲŠØ¯ØŠ.", "tag_people": "ØšŲ„ŲŲ‘Ų… Ø§Ų„ØŖØ´ØŽØ§Øĩ", @@ -2386,13 +2392,14 @@ "view_name": "ØšØąØļ", "view_next_asset": "ØšØąØļ Ø§Ų„Ų…Ø­ØĒŲˆŲ‰ Ø§Ų„ØĒØ§Ų„ŲŠ", "view_previous_asset": "ØšØąØļ Ø§Ų„Ų…Ø­ØĒŲˆŲ‰ Ø§Ų„ØŗØ§Ø¨Ų‚", - "view_qr_code": "Â­ØšØąØļ ØąŲ…Ø˛ Ø§Ų„Ø§ØŗØĒØŦاب؊ Ø§Ų„ØŗØąŲŠØšØŠ", + "view_qr_code": "ØšØąØļ ØąŲ…Ø˛ Ø§Ų„Ø§ØŗØĒØŦاب؊ Ø§Ų„ØŗØąŲŠØšØŠ", "view_similar_photos": "ØšØąØļ ØĩŲˆØą Ų…Ø´Ø§Ø¨Ų‡ØŠ", "view_stack": "ØšØąØļ Ø§Ų„ØĒŲƒØ¯ŲŠØŗ", "view_user": "ØšØąØļ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…", "viewer_remove_from_stack": "Ø­Ø°Ų Ų…Ų† Ø§Ų„ŲƒŲˆŲ…Ų‡ ØŖŲˆ Ø§Ų„Ų…ØŦŲ…ŲˆØšØŠ", "viewer_stack_use_as_main_asset": "Ø§ØŗØĒØŽØ¯Ų… ŲƒØŖØĩŲ„ ØąØĻŲŠØŗŲŠ", "viewer_unstack": "؁؃ Ø§Ų„ŲƒŲˆŲ…Ų‡", + "visibility": "ØĨŲ…ŲƒØ§Ų†ŲŠØŠ Ø§Ų„ØąØ¤ŲŠØŠ", "visibility_changed": "Ø§Ų„ØąØ¤ŲŠØŠ ØĒØēŲŠØąØĒ Ų„Ų€ {count, plural, one {Ø´ØŽØĩ ŲˆØ§Ø­Ø¯} other {# ؚد؊ ØŖØ´ØŽØ§Øĩ}}", "visual": "Ų…ØąØĻ؊", "visual_builder": "ادا؊ Ų†Ø´Ø§ØĄ Ų…ØąØĻŲŠØŠ", @@ -2404,14 +2411,14 @@ "welcome_to_immich": "Ų…ØąØ­Ø¨Ø§Ų‹ Ø¨Ųƒ ؁؊ Immich", "width": "ØšŲØąØļ", "wifi_name": "Ø§ØŗŲ… Ø´Ø¨ŲƒØŠ Wi-Fi", - "workflow_delete_prompt": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ Ų…Ų† Ø­Ø°Ų ØŗŲŠØą Ø§Ų„ØšŲ…Ų„ Ų‡Ø°Ø§ØŸ", + "workflow_delete_prompt": "Ų…ØĒØŖŲƒØ¯ Ų…Ų† Ø­Ø°Ų ØŗŲŠØą Ø§Ų„ØšŲ…Ų„ Ų‡Ø°Ø§ØŸ", "workflow_deleted": "ØĒŲ… Ø­Ø°Ų ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", "workflow_description": "؈Øĩ؁ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", "workflow_info": "Ų…ØšŲ„ŲˆŲ…Ø§ØĒ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", "workflow_json": "؅؄؁ JSON Ų„ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", "workflow_json_help": "Ų‚Ų… بØĒØšØ¯ŲŠŲ„ ØĨؚداداØĒ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„ بØĩ؊ØēØŠ JSON. ØŗØĒØĒŲ… Ų…Ø˛Ø§Ų…Ų†ØŠ Ø§Ų„ØĒØēŲŠŲŠØąØ§ØĒ Ų…Øš ØŖØ¯Ø§ØŠ Ø§Ų„ØĨŲ†Ø´Ø§ØĄ Ø§Ų„Ų…ØąØĻŲŠØŠ.", "workflow_name": "Ø§ØŗŲ… ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", - "workflow_navigation_prompt": "Ų‡Ų„ Ø§Ų†ØĒ Ų…ØĒØ§ŲƒØ¯ Ų…Ų† Ø§Ų„Ų…ØēØ§Ø¯ØąØŠ Ø¨Ø¯ŲˆŲ† Ø­ŲØ¸ Ø§Ų„ØĒØēŲŠŲŠØąØ§ØĒ؟", + "workflow_navigation_prompt": "Ų…ØĒØ§ŲƒØ¯ Ų…Ų† Ø§Ų„Ų…ØēØ§Ø¯ØąØŠ Ø¨Ø¯ŲˆŲ† Ø­ŲØ¸ Ø§Ų„ØĒØēŲŠŲŠØąØ§ØĒ؟", "workflow_summary": "Ų…Ų„ØŽØĩ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", "workflow_update_success": "ØĒŲ… ØĒØ­Ø¯ŲŠØĢ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„ Ø¨Ų†ØŦاح", "workflow_updated": "ØĒŲ… ØĒØ­Ø¯ŲŠØĢ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", diff --git a/i18n/be.json b/i18n/be.json index 2605a382b1..ed66c4e1b3 100644 --- a/i18n/be.json +++ b/i18n/be.json @@ -239,7 +239,7 @@ "user_settings": "НаĐģĐ°Đ´Ņ‹ ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–Đēа", "user_settings_description": "ĐšŅ–Ņ€Đ°Đ˛Đ°ĐŊĐŊĐĩ ĐŊаĐģадаĐŧŅ– ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–Đēа", "version_check_enabled_description": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ ĐŋŅ€Đ°Đ˛ĐĩŅ€Đē҃ вĐĩҀҁҖҖ", - "version_check_implications": "Đ¤ŅƒĐŊĐēŅ†Ņ‹Ņ ĐŋŅ€Đ°Đ˛ĐĩŅ€ĐēŅ– вĐĩҀҁҖҖ ĐŋĐĩŅ€Ņ‹ŅĐ´Ņ‹Ņ‡ĐŊа ĐˇĐ˛ŅŅ€Ņ‚Đ°ĐĩŅ†Ņ†Đ° да github.com", + "version_check_implications": "Đ¤ŅƒĐŊĐēŅ†Ņ‹Ņ ĐŋŅ€Đ°Đ˛ĐĩŅ€ĐēŅ– вĐĩҀҁҖҖ ĐŋĐĩŅ€Ņ‹ŅĐ´Ņ‹Ņ‡ĐŊа ĐˇĐ˛ŅŅ€Ņ‚Đ°ĐĩŅ†Ņ†Đ° да {server}", "version_check_settings": "ĐŸŅ€Đ°Đ˛ĐĩŅ€Đēа вĐĩҀҁҖҖ", "version_check_settings_description": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ/адĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ аĐŋĐ°Đ˛ŅŅˆŅ‡ŅĐŊĐŊŅ– ай ĐŊОваК вĐĩҀҁҖҖ" }, diff --git a/i18n/bg.json b/i18n/bg.json index 4e64363267..024ba3502e 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -333,7 +333,7 @@ "storage_template_migration_description": "ĐŸŅ€Đ¸ĐģĐ°ĐŗĐ°ĐŊĐĩ ĐŊа Ņ‚ĐĩĐēŅƒŅ‰Đ¸Ņ {template} ĐēҊĐŧ ĐŋŅ€ĐĩĐ´Đ¸ŅˆĐŊĐž ĐēĐ°Ņ‡ĐĩĐŊĐ¸Ņ‚Đĩ Ņ„Đ°ĐšĐģОвĐĩ", "storage_template_migration_info": "ШайĐģĐžĐŊа ҉Đĩ ĐŋŅ€ĐĩĐžĐąŅ€Đ°ĐˇŅƒĐ˛Đ° Đ˛ŅĐ¸Ņ‡Đēи Ņ€Đ°ĐˇŅˆĐ¸Ņ€ĐĩĐŊĐ¸Ņ ĐŊа иĐŧĐĩĐŊĐ°Ņ‚Đ° ĐŊа Ņ„Đ°ĐšĐģОвĐĩŅ‚Đĩ в Đ´ĐžĐģĐĩĐŊ Ņ€ĐĩĐŗĐ¸ŅŅ‚ŅŠŅ€. ĐŸŅ€ĐžĐŧĐĩĐŊĐ¸Ņ‚Đĩ в ŅˆĐ°ĐąĐģĐžĐŊĐ¸Ņ‚Đĩ ҉Đĩ ҁĐĩ ĐŋŅ€Đ¸ĐģĐ°ĐŗĐ°Ņ‚ ŅĐ°ĐŧĐž Са ĐŊОви ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸. За да ĐŋŅ€Đ¸ĐģĐžĐļĐ¸Ņ‚Đĩ ĐŋŅ€Đ¸ĐŊŅƒĐ´Đ¸Ņ‚ĐĩĐģĐŊĐž ŅˆĐ°ĐąĐģĐžĐŊа ĐēҊĐŧ вĐĩ҇Đĩ ĐēĐ°Ņ‡ĐĩĐŊи ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸, иСĐŋҊĐģĐŊĐĩŅ‚Đĩ {job}.", "storage_template_migration_job": "Đ—Đ°Đ´Đ°Ņ‡Đ° Са ĐŧĐ¸ĐŗŅ€Đ°Ņ†Đ¸Ņ ĐŊа ŅˆĐ°ĐąĐģĐžĐŊа Са ŅŅŠŅ…Ņ€Đ°ĐŊĐĩĐŊиĐĩ", - "storage_template_more_details": "За ĐŋОвĐĩ҇Đĩ ĐŋĐžĐ´Ņ€ĐžĐąĐŊĐžŅŅ‚Đ¸ ĐžŅ‚ĐŊĐžŅĐŊĐž Ņ‚Đ°ĐˇĐ¸ Ņ„ŅƒĐŊĐēŅ†Đ¸Ņ ҁĐĩ ĐžĐąŅŠŅ€ĐŊĐĩŅ‚Đĩ ĐēҊĐŧ ŅˆĐ°ĐąĐģĐžĐŊа Storage Template и ĐŊĐĩĐŗĐžĐ˛Đ¸Ņ‚Đĩ ĐŋĐžŅĐģĐĩĐ´ŅŅ‚Đ˛Đ¸Ņ ", + "storage_template_more_details": "За ĐŋОвĐĩ҇Đĩ ĐŋĐžĐ´Ņ€ĐžĐąĐŊĐžŅŅ‚Đ¸ ĐžŅ‚ĐŊĐžŅĐŊĐž Ņ‚Đ°ĐˇĐ¸ Ņ„ŅƒĐŊĐēŅ†Đ¸Ņ ҁĐĩ ĐžĐąŅŠŅ€ĐŊĐĩŅ‚Đĩ ĐēҊĐŧ ŅˆĐ°ĐąĐģĐžĐŊа Storage Template и ĐŊĐĩĐŗĐžĐ˛Đ¸Ņ‚Đĩ ĐŋĐžŅĐģĐĩĐ´ŅŅ‚Đ˛Đ¸Ņ", "storage_template_onboarding_description_v2": "ĐšĐžĐŗĐ°Ņ‚Đž Đĩ Ņ€Đ°ĐˇŅ€Đĩ҈ĐĩĐŊа, Ņ‚Đ°ĐˇĐ¸ Ņ„ŅƒĐŊĐēŅ†Đ¸Ņ ҉Đĩ ĐžŅ€ĐŗĐ°ĐŊĐ¸ĐˇĐ¸Ņ€Đ° Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž Ņ„Đ°ĐšĐģОвĐĩŅ‚Đĩ, ҁĐŋĐžŅ€ĐĩĐ´ ŅˆĐ°ĐąĐģĐžĐŊ, Đ´ĐĩŅ„Đ¸ĐŊĐ¸Ņ€Đ°ĐŊ ĐžŅ‚ ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģŅ. За Đ´ĐžĐŋҊĐģĐŊĐ¸Ņ‚ĐĩĐģĐŊа иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ, ĐŧĐžĐģŅ виĐļŅ‚Đĩ Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Đ¸ŅŅ‚Đ°.", "storage_template_path_length": "ĐžĐŗŅ€Đ°ĐŊĐ¸Ņ‡ĐĩĐŊиĐĩ ĐŊа Đ´ŅŠĐģĐļиĐŊĐ°Ņ‚Đ° ĐŊа ĐŋŅŠŅ‚Ņ: {length, number}/{limit, number}", "storage_template_settings": "ШайĐģĐžĐŊ Са ŅŅŠŅ…Ņ€Đ°ĐŊĐĩĐŊиĐĩ", @@ -441,7 +441,7 @@ "user_successfully_removed": "ĐŸĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģ {email} Đĩ ҃ҁĐŋĐĩ҈ĐŊĐž ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊĐ°Ņ‚.", "users_page_description": "ĐĄŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Đ° Са адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģи", "version_check_enabled_description": "АĐēŅ‚Đ¸Đ˛Đ¸Ņ€Đ°Đš ĐŋŅ€ĐžĐ˛ĐĩŅ€Đēа ĐŊа вĐĩŅ€ŅĐ¸ŅŅ‚Đ°", - "version_check_implications": "Đ¤ŅƒĐŊĐēŅ†Đ¸ŅŅ‚Đ° Са ĐŋŅ€ĐžĐ˛ĐĩŅ€Đēа ĐŊа вĐĩŅ€ŅĐ¸ŅŅ‚Đ° Ņ€Đ°ĐˇŅ‡Đ¸Ņ‚Đ° ĐŊа ĐŋĐĩŅ€Đ¸ĐžĐ´Đ¸Ņ‡ĐŊа ĐēĐžĐŧ҃ĐŊиĐēĐ°Ņ†Đ¸Ņ ҁ github.com", + "version_check_implications": "Đ¤ŅƒĐŊĐēŅ†Đ¸ŅŅ‚Đ° Са ĐŋŅ€ĐžĐ˛ĐĩŅ€Đēа ĐŊа вĐĩŅ€ŅĐ¸ŅŅ‚Đ° Ņ€Đ°ĐˇŅ‡Đ¸Ņ‚Đ° ĐŊа ĐŋĐĩŅ€Đ¸ĐžĐ´Đ¸Ņ‡ĐŊа ĐēĐžĐŧ҃ĐŊиĐēĐ°Ņ†Đ¸Ņ ҁ {server}", "version_check_settings": "ĐŸŅ€ĐžĐ˛ĐĩŅ€Đēа ĐŊа вĐĩŅ€ŅĐ¸ŅŅ‚Đ°", "version_check_settings_description": "АĐēŅ‚Đ¸Đ˛Đ¸Ņ€Đ°ĐšŅ‚Đĩ/Đ´ĐĩаĐēŅ‚Đ¸Đ˛Đ¸Ņ€Đ°ĐšŅ‚Đĩ иСвĐĩŅŅ‚Đ¸ĐĩŅ‚Đž Са ĐŊОва вĐĩŅ€ŅĐ¸Ņ", "video_conversion_job": "ĐĸŅ€Đ°ĐŊҁĐēĐžĐ´Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа видĐĩĐžĐēĐģиĐŋОвĐĩŅ‚Đĩ", @@ -849,9 +849,12 @@ "create_link_to_share": "ĐĄŅŠĐˇĐ´Đ°Đ˛Đ°ĐŊĐĩ ĐŊа ĐģиĐŊĐē Са ҁĐŋОдĐĩĐģŅĐŊĐĩ", "create_link_to_share_description": "ПозвоĐģĐĩŅ‚Đĩ ĐŊа Đ˛ŅĐĩĐēи, ĐēĐžĐšŅ‚Đž иĐŧа ĐģиĐŊĐē, да види Đ¸ĐˇĐąŅ€Đ°ĐŊĐ°Ņ‚Đ°(Đ¸Ņ‚Đĩ) ҁĐŊиĐŧĐēа(и)", "create_new": "ĐĄĐĒЗДАЙ НОВ", + "create_new_face": "ĐĄŅŠĐˇĐ´Đ°Đš ĐŊОвО ĐģĐ¸Ņ†Đĩ", "create_new_person": "ĐĄŅŠĐˇĐ´Đ°Đ˛Đ°ĐŊĐĩ ĐŊа ĐŊОвО ĐģĐ¸Ņ†Đĩ", "create_new_person_hint": "ĐŸŅ€Đ¸ŅĐ˛ĐžĐšŅ‚Đĩ Đ¸ĐˇĐąŅ€Đ°ĐŊĐ¸Ņ‚Đĩ Ņ„Đ°ĐšĐģОвĐĩ ĐŊа ĐŊОв Ņ‡ĐžĐ˛ĐĩĐē", "create_new_user": "ĐĄŅŠĐˇĐ´Đ°Đ˛Đ°ĐŊĐĩ ĐŊа ĐŊОв ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģ", + "create_person": "ĐĄŅŠĐˇĐ´Đ°Đš Ņ‡ĐžĐ˛ĐĩĐē", + "create_person_subtitle": "Добави иĐŧĐĩ ĐēҊĐŧ Đ¸ĐˇĐąŅ€Đ°ĐŊĐžŅ‚Đž ĐģĐ¸Ņ†Đĩ Са да ŅŅŠĐˇĐ´Đ°Đ´Đĩ҈ и да ҁĐģĐžĐļĐ¸Ņˆ ĐĩŅ‚Đ¸ĐēĐĩŅ‚ ĐŊа ĐŊĐžĐ˛Đ¸Ņ Ņ‡ĐžĐ˛ĐĩĐē", "create_shared_album_page_share_add_assets": "ДОБАВИ ОБЕКĐĸИ", "create_shared_album_page_share_select_photos": "ИСйĐĩŅ€Đ¸ ҁĐŊиĐŧĐēи", "create_shared_link": "ĐĄŅŠĐˇĐ´Đ°Đš ĐģиĐŊĐē Са ҁĐŋОдĐĩĐģŅĐŊĐĩ", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "ФиĐēŅĐ¸Ņ€Đ°ĐŊ", "crop_aspect_ratio_free": "ХвОйОдĐĩĐŊ", "crop_aspect_ratio_original": "ĐžŅ€Đ¸ĐŗĐ¸ĐŊаĐģĐĩĐŊ", + "crop_aspect_ratio_square": "ĐšĐ˛Đ°Đ´Ņ€Đ°Ņ‚", "curated_object_page_title": "НĐĩŅ‰Đ°", "current_device": "ĐĸĐĩĐēŅƒŅ‰Đž ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đž", "current_pin_code": "ĐĄĐĩĐŗĐ°ŅˆĐĩĐŊ PIN ĐēОд", @@ -880,7 +884,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM yyyy", "dark": "ĐĸҊĐŧĐĩĐŊ", - "dark_theme": "ĐĸҊĐŧĐŊа Ņ‚ĐĩĐŧа", + "dark_theme": "ĐŸŅ€ĐĩĐŧиĐŊи ĐēҊĐŧ Ņ‚ŅŠĐŧĐŊа Ņ‚ĐĩĐŧа", "date": "Đ”Đ°Ņ‚Đ°", "date_after": "Đ”Đ°Ņ‚Đ° ҁĐģĐĩĐ´", "date_and_time": "Đ”Đ°Ņ‚Đ° и Ņ‡Đ°Ņ", @@ -891,10 +895,8 @@ "day": "ДĐĩĐŊ", "days": "ДĐŊи", "deduplicate_all": "ДĐĩĐ´ŅƒĐŋĐģиĐēĐ¸Ņ€Đ°ĐŊĐĩ ĐŊа Đ˛ŅĐ¸Ņ‡Đēи", - "deduplication_criteria_1": "РаСĐŧĐĩŅ€ ĐŊа ҁĐŊиĐŧĐēĐ°Ņ‚Đ° в ĐąĐ°ĐšŅ‚ĐžĐ˛Đĩ", - "deduplication_criteria_2": "Đ‘Ņ€ĐžĐš EXIF даĐŊĐŊи", - "deduplication_info": "ИĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Са Đ´ĐĩĐ´ŅƒĐŋĐģиĐēĐ°Ņ†Đ¸ŅŅ‚Đ°", - "deduplication_info_description": "За Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž ĐŋŅ€ĐĩĐ´Đ˛Đ°Ņ€Đ¸Ņ‚ĐĩĐģĐŊĐž Đ¸ĐˇĐąĐ¸Ņ€Đ°ĐŊĐĩ ĐŊа Ņ€ĐĩŅŅƒŅ€ŅĐ¸ и ĐŋŅ€ĐĩĐŧĐ°Ņ…Đ˛Đ°ĐŊĐĩ ĐŊа Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Đ¸ ĐŊа ĐĩĐ´Ņ€Đž, Ņ€Đ°ĐˇĐŗĐģĐĩĐļдаĐŧĐĩ:", + "default_locale": "ЕзиĐē ĐŋĐž ĐŋĐžĐ´Ņ€Đ°ĐˇĐąĐ¸Ņ€Đ°ĐŊĐĩ", + "default_locale_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚ ĐŊа Đ´Đ°Ņ‚Đ° и Ņ‡Đ¸ŅĐģа ҁĐŋĐžŅ€ĐĩĐ´ ĐĩСиĐēĐžĐ˛Đ°Ņ‚Đ° ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēа ĐŊа ĐąŅ€Đ°ŅƒĐˇŅŠŅ€Đ°", "delete": "Đ˜ĐˇŅ‚Ņ€Đ¸Đš", "delete_action_confirmation_message": "ĐĄĐ¸ĐŗŅƒŅ€ĐŊи Đģи ҁ҂Đĩ, ҇Đĩ Đ¸ŅĐēĐ°Ņ‚Đĩ да Đ¸ĐˇŅ‚Ņ€Đ¸ĐĩŅ‚Đĩ Ņ‚ĐžĐˇĐ¸ ОйĐĩĐēŅ‚? ĐĄĐģĐĩдва ĐŋŅ€ĐĩĐŧĐĩŅŅ‚Đ˛Đ°ĐŊĐĩ ĐŊа ОйĐĩĐēŅ‚Đ° в ĐēĐžŅˆĐ° Са ĐžŅ‚ĐŋĐ°Đ´ŅŠŅ†Đ¸ ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ° и ҉Đĩ ĐŋĐžĐģŅƒŅ‡Đ¸Ņ‚Đĩ ĐŋŅ€ĐĩĐ´ĐģĐžĐļĐĩĐŊиĐĩ ОйĐĩĐēŅ‚Đ° да ĐąŅŠĐ´Đĩ Đ¸ĐˇŅ‚Ņ€Đ¸Ņ‚ ĐģĐžĐēаĐģĐŊĐž", "delete_action_prompt": "{count} ŅĐ° Đ¸ĐˇŅ‚Ņ€Đ¸Ņ‚Đ¸", @@ -970,7 +972,7 @@ "downloading_media": "Đ˜ĐˇŅ‚ĐĩĐŗĐģŅĐŊĐĩ ĐŊа ĐŧĐĩĐ´Đ¸Ņ", "drop_files_to_upload": "ĐŸŅƒŅĐŊĐĩŅ‚Đĩ Ņ„Đ°ĐšĐģОвĐĩŅ‚Đĩ, Са да ĐŗĐ¸ ĐēĐ°Ņ‡Đ¸Ņ‚Đĩ", "duplicates": "Đ”ŅƒĐąĐģиĐēĐ°Ņ‚Đ¸", - "duplicates_description": "ИСйĐĩŅ€ĐĩŅ‚Đĩ Đ˛ŅŅĐēа ĐŗŅ€ŅƒĐŋа, ĐēĐ°Ņ‚Đž ĐŋĐžŅĐžŅ‡Đ¸Ņ‚Đĩ ĐēОи, аĐēĐž иĐŧа Ņ‚Đ°Đēива, ŅĐ° Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Đ¸", + "duplicates_description": "ИСйĐĩŅ€ĐĩŅ‚Đĩ Đ˛ŅŅĐēа ĐŗŅ€ŅƒĐŋа, ĐēĐ°Ņ‚Đž ĐŋĐžŅĐžŅ‡Đ¸Ņ‚Đĩ ĐēОи, аĐēĐž иĐŧа Ņ‚Đ°Đēива, ŅĐ° Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Đ¸.", "duration": "ĐŸŅ€ĐžĐ´ŅŠĐģĐļĐ¸Ņ‚ĐĩĐģĐŊĐžŅŅ‚", "edit": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€Đ°ĐŊĐĩ", "edit_album": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа аĐģĐąŅƒĐŧ", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "Đ—Đ°ĐŗĐģавиĐĩ ĐŊа аĐģĐąŅƒĐŧа", "licenses": "Đ›Đ¸Ņ†ĐĩĐŊСи", "light": "ХвĐĩŅ‚ĐģĐž", + "light_theme": "ĐŸŅ€ĐĩĐŧиĐŊи ĐēҊĐŧ ŅĐ˛ĐĩŅ‚Đģа Ņ‚ĐĩĐŧа", "like": "ĐĨĐ°Ņ€ĐĩŅĐ°ĐšŅ‚Đĩ", "like_deleted": "ĐšĐ°Ņ‚Đž Đ¸ĐˇŅ‚Ņ€Đ¸Ņ‚", "link_motion_video": "ЛиĐŊĐē ĐēҊĐŧ видĐĩĐž", + "link_to_docs": "За ĐŋОвĐĩ҇Đĩ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ виĐļŅ‚Đĩ Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Đ¸ŅŅ‚Đ°.", "link_to_oauth": "ЛиĐŊĐē ĐēҊĐŧ OAuth", "linked_oauth_account": "ĐĄĐ˛ŅŠŅ€ĐˇĐ°ĐŊ OAuth аĐēĐ°ŅƒĐŊŅ‚", "list": "Đ›Đ¸ŅŅ‚", @@ -1651,13 +1655,14 @@ "only_favorites": "ХаĐŧĐž ĐģŅŽĐąĐ¸Đŧи", "open": "ĐžŅ‚Đ˛ĐžŅ€Đ¸", "open_calendar": "ĐžŅ‚Đ˛ĐžŅ€Đ¸ ĐēаĐģĐĩĐŊĐ´Đ°Ņ€", + "open_in_browser": "ĐžŅ‚Đ˛ĐžŅ€Đ¸ в ĐąŅ€Đ°ŅƒĐˇŅŠŅ€", "open_in_map_view": "ĐžŅ‚Đ˛ĐžŅ€Đ¸ Đ¸ĐˇĐŗĐģĐĩĐ´ ĐŊа ĐēĐ°Ņ€Ņ‚Đ°", "open_in_openstreetmap": "ĐžŅ‚Đ˛ĐžŅ€Đ¸ в OpenStreetMap", "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_into_albums": "ĐŸĐžĐ´Ņ€ĐĩĐ´ĐĩŅ‚Đĩ в аĐģĐąŅƒĐŧи", + "organize_into_albums_description": "ДобавĐĩŅ‚Đĩ ĐŊаĐģĐ¸Ņ‡ĐŊĐ¸Ņ‚Đĩ ҁĐŊиĐŧĐēи в аĐģĐąŅƒĐŧи, ĐēĐ°Ņ‚Đž иСĐŋĐžĐģĐˇĐ˛Đ°Ņ‚Đĩ Ņ‚ĐĩĐēŅƒŅ‰Đ¸Ņ‚Đĩ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēи Са ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ¸Ņ€Đ°ĐŊĐĩ", "organize_your_library": "ĐžŅ€ĐŗĐ°ĐŊĐ¸ĐˇĐ¸Ņ€Đ°ĐŊĐĩ ĐŊа Đ˛Đ°ŅˆĐ°Ņ‚Đ° йийĐģĐ¸ĐžŅ‚ĐĩĐēа", "original": "ĐžŅ€Đ¸ĐŗĐ¸ĐŊаĐģ", "other": "Đ”Ņ€ŅƒĐŗĐ¸", @@ -1805,7 +1810,7 @@ "purchase_server_description_2": "ĐĄŅ‚Đ°Ņ‚ŅƒŅ ĐŊа ĐŋĐžĐ´Đ´Ņ€ŅŠĐļĐŊиĐē", "purchase_server_title": "ĐĄŅŠŅ€Đ˛ŅŠŅ€", "purchase_settings_server_activated": "ĐŸŅ€ĐžĐ´ŅƒĐēŅ‚ĐžĐ˛Đ¸ŅŅ‚ ĐēĐģŅŽŅ‡ ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ° ҁĐĩ ҃ĐŋŅ€Đ°Đ˛ĐģŅĐ˛Đ° ĐžŅ‚ адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", - "query_asset_id": "Buscar item per ID", + "query_asset_id": "ĐĸŅŠŅ€ŅĐĩĐŊĐĩ ĐŊа ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ ĐŋĐž ID", "queue_status": "В ĐžĐŋĐ°ŅˆĐēа {count} ĐžŅ‚ {total}", "rate_asset": "ЗадаваĐŊĐĩ ĐŊа Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", "rating": "ĐžŅ†ĐĩĐŊĐēа ҁҊҁ СвĐĩСди", @@ -2212,6 +2217,7 @@ "tag": "ĐĸĐ°Đŗ", "tag_assets": "ĐĸĐ°ĐŗĐŊи ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", "tag_created": "ĐĄŅŠĐˇĐ´Đ°Đ´ĐĩĐŊ ĐĩŅ‚Đ¸ĐēĐĩŅ‚: {tag}", + "tag_face": "ĐžŅ‚ĐąĐĩĐģĐĩĐļи ĐģĐ¸Ņ†Đĩ", "tag_feature_description": "Đ Đ°ĐˇĐŗĐģĐĩĐļдаĐŊĐĩ ĐŊа ҁĐŊиĐŧĐēи и видĐĩĐžĐēĐģиĐŋОвĐĩ, ĐŗŅ€ŅƒĐŋĐ¸Ņ€Đ°ĐŊи ĐŋĐž Ņ‚ĐĩĐŧи ҁ ĐģĐžĐŗĐ¸Ņ‡ĐĩҁĐēи Ņ‚Đ°ĐŗĐžĐ˛Đĩ", "tag_not_found_question": "НĐĩ ĐŧĐžĐļĐĩŅ‚Đĩ да ĐŊаĐŧĐĩŅ€Đ¸Ņ‚Đĩ ĐĩŅ‚Đ¸ĐēĐĩŅ‚? ĐĄŅŠĐˇĐ´Đ°ĐšŅ‚Đĩ ĐŊОв ĐĩŅ‚Đ¸ĐēĐĩŅ‚.", "tag_people": "ĐžŅ‚ĐąĐĩĐģĐĩĐļи ĐĨĐžŅ€Đ°", @@ -2393,6 +2399,7 @@ "viewer_remove_from_stack": "ĐŸŅ€ĐĩĐŧĐ°Ņ…Đ˛Đ°ĐŊĐĩ ĐžŅ‚ ĐžĐŋĐ°ŅˆĐēĐ°Ņ‚Đ°", "viewer_stack_use_as_main_asset": "ИСĐŋĐžĐģСваК ĐēĐ°Ņ‚Đž ĐžŅĐŊОвĐĩĐŊ", "viewer_unstack": "ĐŸŅ€ĐĩĐŧĐ°Ņ…ĐŊи ĐžŅ‚ ĐžĐŋĐ°ŅˆĐēĐ°Ņ‚Đ°", + "visibility": "ВидиĐŧĐžŅŅ‚", "visibility_changed": "ВидиĐŧĐžŅŅ‚Ņ‚Đ° Đĩ ĐŋŅ€ĐžĐŧĐĩĐŊĐĩĐŊа Са {count, plural, one {# Ņ‡ĐžĐ˛ĐĩĐē} other {# Ņ‡ĐžĐ˛ĐĩĐēа}}", "visual": "Đ’Đ¸ĐˇŅƒĐ°ĐģĐĩĐŊ", "visual_builder": "Đ’Đ¸ĐˇŅƒĐ°ĐģĐĩĐŊ ĐēĐžĐŊŅŅ‚Ņ€ŅƒĐēŅ‚ĐžŅ€", diff --git a/i18n/bn.json b/i18n/bn.json index 4580ca5551..dcc6834323 100644 --- a/i18n/bn.json +++ b/i18n/bn.json @@ -231,6 +231,8 @@ "metadata_settings_description": "āĻŽā§‡āϟāĻžāĻĄā§‡āϟāĻž āϏ⧇āϟāĻŋāĻ‚āϏ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ (Manage metadata settings)", "migration_job": "āĻŽāĻžāχāĻ—ā§āϰ⧇āĻļāύ (Migration)", "migration_job_description": "āĻ…ā§āϝāĻžāϏ⧇āϟ āĻāĻŦāĻ‚ āĻĢ⧇āϏ āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞāϗ⧁āϞ⧋āϕ⧇ āϏāĻ°ā§āĻŦāĻļ⧇āώ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ āĻ¸ā§āĻŸā§āϰāĻžāĻ•āϚāĻžāϰ⧇ āĻŽāĻžāχāĻ—ā§āϰ⧇āϟ āĻ•āϰ⧁āύāĨ¤ (Migrate thumbnails for assets and faces to the latest folder structure)", + "nightly_tasks_cluster_faces_setting_description": "āύāϤ⧁āύ āĻļāύāĻžāĻ•ā§āϤ āĻšāĻ“āϝāĻŧāĻž āĻŽā§āĻ–āϗ⧁āϞāĻŋāϤ⧇ āĻĢ⧇āϏāĻŋāϝāĻŧāĻžāϞ āϰāĻŋāĻ•āĻ—āύāĻŋāĻļāύ āϚāĻžāϞāĻžāύ", + "nightly_tasks_cluster_new_faces_setting": "āύāϤ⧁āύ āĻŽā§āĻ–āϗ⧁āϞ⧋āϰ āϗ⧁āĻšā§āĻ›", "nightly_tasks_database_cleanup_setting": "āĻĄā§‡āϟāĻžāĻŦ⧇āϏ āĻ•ā§āϞāĻŋāύāφāĻĒ āϟāĻžāĻ¸ā§āĻ•āϏāĻŽā§‚āĻš (Database cleanup tasks)", "nightly_tasks_database_cleanup_setting_description": "āĻĄā§‡āϟāĻžāĻŦ⧇āϏ āĻĨ⧇āϕ⧇ āĻĒ⧁āϰ⧋āύ⧋ āĻāĻŦāĻ‚ āĻŽā§‡ā§ŸāĻžāĻĻā§‹āĻ¤ā§āϤ⧀āĻ°ā§āĻŖ āĻĄā§‡āϟāĻž āĻŽā§āϛ⧇ āĻĢ⧇āϞ⧁āύ", "nightly_tasks_generate_memories_setting": "āĻŽā§‡āĻŽā§‹āϰāĻŋāϜ āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ (Generate memories)", @@ -257,6 +259,20 @@ "notification_email_secure": "SMTPS (āĻ¸ā§āĻŽāĻžāĻ°ā§āϟ āĻŽā§‡āχāϞ āĻŸā§āϰāĻžāĻ¨ā§āϏāĻĢāĻžāϰ āĻĒā§āϰ⧋āĻŸā§‹āĻ•āϞ āϏāĻŋāĻ•āĻŋāωāϰ)", "notification_email_secure_description": "SMTPS (SMTP over TLS) āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧁āύ", "notification_email_sent_test_email_button": "āĻŸā§‡āĻ¸ā§āϟ āχāĻŽā§‡āϞ āĻĒāĻžāĻ āĻžāύ āĻāĻŦāĻ‚ āϏ⧇āĻ­ āĻ•āϰ⧁āύ", + "notification_email_setting_description": "āχāĻŽā§‡āϞ āύ⧋āϟāĻŋāĻĢāĻŋāϕ⧇āĻļāύ āĻĒāĻžāĻ āĻžāύ⧋āϰ āϏ⧇āϟāĻŋāĻ‚āϏ", + "notification_email_test_email": "āĻĒāϰ⧀āĻ•ā§āώāĻžāĻŽā§‚āϞāĻ• āχāĻŽā§‡āχāϞ āĻĒāĻžāĻ āĻžāύ", + "notification_email_test_email_failed": "āĻĒāϰ⧀āĻ•ā§āώāĻžāĻŽā§‚āϞāĻ• āχāĻŽā§‡āϞ āĻĒāĻžāĻ āĻžāύ⧋ āϏāĻŽā§āĻ­āĻŦ āĻšāϝāĻŧāύāĻŋ, āφāĻĒāύāĻžāϰ āϏ⧇āϟāĻŋāĻ‚āϏ āϝāĻžāϚāĻžāχ āĻ•āϰ⧁āύ", + "notification_email_test_email_sent": "{email}-āĻ āĻāĻ•āϟāĻŋ āĻĒāϰ⧀āĻ•ā§āώāĻžāĻŽā§‚āϞāĻ• āχāĻŽā§‡āϞ āĻĒāĻžāĻ āĻžāύ⧋ āĻšāϝāĻŧ⧇āϛ⧇āĨ¤ āĻ…āύ⧁āĻ—ā§āϰāĻš āĻ•āϰ⧇ āφāĻĒāύāĻžāϰ āχāύāĻŦāĻ•ā§āϏ āĻĻ⧇āϖ⧁āύāĨ¤", + "notification_email_username_description": "āχāĻŽā§‡āϞ āϏāĻžāĻ°ā§āĻ­āĻžāϰ⧇ āϭ⧇āϰāĻŋāĻĢāĻŋāϕ⧇āϏāύ⧇āϰ āϜāĻ¨ā§āϝ āĻŦā§āϝāĻŦāĻšā§ƒāϤ āχāωāϜāĻžāϰāύ⧇āĻŽ", + "notification_enable_email_notifications": "āχāĻŽā§‡āϞ āύ⧋āϟāĻŋāĻĢāĻŋāϕ⧇āϏāύ āϏāĻ•ā§āϰāĻŋāϝāĻŧ āĻ•āϰ⧁āύ", + "notification_settings": "āύ⧋āϟāĻŋāĻĢāĻŋāϕ⧇āϏāύ āϏ⧇āϟāĻŋāĻ‚āϏ", + "notification_settings_description": "āχāĻŽā§‡āχāϞ āϏāĻš āύ⧋āϟāĻŋāĻĢāĻŋāϕ⧇āĻļāύ āϏ⧇āϟāĻŋāĻ‚āϏ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ", + "oauth_auto_launch": "āĻ…āĻŸā§‹ āϞāĻžā§āϚ", + "oauth_auto_launch_description": "āϞāĻ—āχāύ āĻĒ⧇āĻœā§‡ āĻĒā§āϰāĻŦ⧇āĻļ āĻ•āϰāĻžāϰ āϏāĻžāĻĨ⧇ āϏāĻžāĻĨ⧇ OAuth āϞāĻ—āχāύ āĻĒā§āϰāĻ•ā§āϰāĻŋāϝāĻŧāĻžāϟāĻŋ āĻ¸ā§āĻŦāϝāĻŧāĻ‚āĻ•ā§āϰāĻŋāϝāĻŧāĻ­āĻžāĻŦ⧇ āĻļ⧁āϰ⧁ āĻ•āϰ⧁āύ", + "oauth_auto_register": "āϏ⧟āĻ‚āĻ•ā§āϰāĻŋ⧟āĻ­āĻžāĻŦ⧇ āϰ⧇āϜāĻŋāĻ¸ā§āϟāĻžāϰ āĻ•āϰ⧁āύ", + "oauth_auto_register_description": "OAuth āĻĻāĻŋāϝāĻŧ⧇ āϏāĻžāχāύ āχāύ āĻ•āϰāĻžāϰ āĻĒāϰ āύāϤ⧁āύ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āĻĻ⧇āϰ āĻ¸ā§āĻŦāϝāĻŧāĻ‚āĻ•ā§āϰāĻŋāϝāĻŧāĻ­āĻžāĻŦ⧇ āύāĻŋāĻŦāĻ¨ā§āϧāύ āĻ•āϰ⧁āύ", + "oauth_button_text": "āĻŦāĻžāϟāύ āĻŸā§‡āĻ•ā§āϏāϟ", + "oauth_client_secret_description": "āĻ—ā§‹āĻĒāύ⧀āϝāĻŧ āĻ•ā§āϞāĻžāϝāĻŧ⧇āĻ¨ā§āĻŸā§‡āϰ āϜāĻ¨ā§āϝ āĻĒā§āϰāϝāĻŧā§‹āϜāύ, āĻ…āĻĨāĻŦāĻž āϝāĻĻāĻŋ āĻĒāĻžāĻŦāϞāĻŋāĻ• āĻ•ā§āϞāĻžāϝāĻŧ⧇āĻ¨ā§āĻŸā§‡āϰ āϜāĻ¨ā§āϝ PKCE (Proof Key for Code Exchange) āϏāĻŽāĻ°ā§āĻĨāĻŋāϤ āύāĻž āĻšāϝāĻŧāĨ¤", "oauth_enable_description": "OAuth-āĻāϰ āĻŽāĻžāĻ§ā§āϝāĻŽā§‡ āϞāĻ—āχāύ āĻ•āϰ⧁āύ", "oauth_mobile_redirect_uri": "āĻŽā§‹āĻŦāĻžāχāϞ āϰāĻŋāĻĄāĻžāχāϰ⧇āĻ•ā§āϟ āχāωāφāϰāφāχ (URI)", "oauth_mobile_redirect_uri_override": "āĻŽā§‹āĻŦāĻžāχāϞ āϰāĻŋāĻĄāĻžāχāϰ⧇āĻ•ā§āϟ āχāωāφāϰāφāχ (URI) āĻ“āĻ­āĻžāϰāϰāĻžāχāĻĄ", @@ -323,6 +339,20 @@ "storage_template_settings": "āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āϟ (Storage Template)", "storage_template_settings_description": "āφāĻĒāϞ⧋āĻĄ āĻ•āϰāĻž āĻ…ā§āϝāĻžāϏ⧇āĻŸā§‡āϰ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ āĻ¸ā§āĻŸā§āϰāĻžāĻ•āϚāĻžāϰ āĻāĻŦāĻ‚ āĻĢāĻžāχāϞ āύ⧇āĻŽ āĻŽā§āϝāĻžāύ⧇āϜ āĻ•āϰ⧁āύ", "storage_template_user_label": "{label} āĻšāϞ⧋ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰ āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āϞ⧇āĻŦ⧇āϞ (Storage Label)", + "system_settings": "āϏāĻŋāĻ¸ā§āĻŸā§‡āĻŽ āϏ⧇āϟāĻŋāĻ‚āϏ", + "tag_cleanup_job": "āĻŸā§āϝāĻžāĻ— āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž", + "template_email_available_tags": "āφāĻĒāύāĻŋ āφāĻĒāύāĻžāϰ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āĻŸā§‡ āύāĻŋāĻŽā§āύāϞāĻŋāĻ–āĻŋāϤ āϭ⧇āϰāĻŋāϝāĻŧ⧇āĻŦāϞāϗ⧁āϞ⧋ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰāϤ⧇ āĻĒāĻžāϰ⧇āύ: {tags}", + "template_email_if_empty": "āĻŸā§‡āĻŽāĻĒā§āϞ⧇āϟāϟāĻŋ āĻ–āĻžāϞāĻŋ āĻĨāĻžāĻ•āϞ⧇ āĻĄāĻŋāĻĢāĻ˛ā§āϟ āχāĻŽā§‡āϞ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤", + "template_email_invite_album": "āχāύāĻ­āĻžāχāϟ āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āϟ", + "template_email_preview": "āĻĒā§āϰāĻŋāĻ­āĻŋāω", + "template_email_settings": "āχāĻŽā§‡āχāϞ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āϟ", + "template_email_update_album": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āϟ āφāĻĒāĻĄā§‡āϟ āĻ•āϰ⧁āύ", + "template_email_welcome": "āĻ¸ā§āĻŦāĻžāĻ—āϤāĻŽ āχāĻŽā§‡āχāϞ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āϟ", + "template_settings": "āύ⧋āϟāĻŋāĻĢāĻŋāϕ⧇āĻļāύ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āϟ", + "template_settings_description": "āύ⧋āϟāĻŋāĻĢāĻŋāϕ⧇āĻļāύ⧇āϰ āϜāĻ¨ā§āϝ āĻ•āĻžāĻ¸ā§āϟāĻŽ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āϟ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ", + "theme_custom_css_settings": "āĻ•āĻžāĻ¸ā§āϟāĻŽ CSS", + "theme_custom_css_settings_description": "āĻ•ā§āϝāĻžāϏāϕ⧇āĻĄāĻŋāĻ‚ āĻ¸ā§āϟāĻžāχāϞ āĻļā§€āϟ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧇ Immich āĻāϰ āĻĄāĻŋāϜāĻžāχāύ āĻ•āĻžāĻ¸ā§āϟāĻŽāĻžāχāϜ āĻ•āϰāĻž āϝāĻžāϝāĻŧāĨ¤", + "theme_settings": "āĻĨā§€āĻŽ āϏ⧇āϟāĻŋāĻ‚āϏ", "theme_settings_description": "āχāĻŽāĻŋāϚ (Immich) āĻ“āϝāĻŧ⧇āĻŦ āχāĻ¨ā§āϟāĻžāϰāĻĢ⧇āϏ⧇āϰ āĻ•āĻžāĻ¸ā§āϟāĻŽāĻžāχāĻœā§‡āĻļāύ āĻŽā§āϝāĻžāύ⧇āϜ āĻ•āϰ⧁āύ", "thumbnail_generation_job": "āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞ āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ (Generate Thumbnails)", "thumbnail_generation_job_description": "āĻĒā§āϰāϤāĻŋāϟāĻŋ āĻ…ā§āϝāĻžāϏ⧇āĻŸā§‡āϰ āϜāĻ¨ā§āϝ āĻŦ⧜, āϛ⧋āϟ āĻāĻŦāĻ‚ āĻŦā§āϞāĻžāϰ (āĻ…āĻ¸ā§āĻĒāĻˇā§āϟ) āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞ āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ, āϏ⧇āχ āϏāĻžāĻĨ⧇ āĻĒā§āϰāϤāĻŋāϟāĻŋ āĻŦā§āϝāĻ•ā§āϤāĻŋāϰ āϜāĻ¨ā§āϝāĻ“ āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞ āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύāĨ¤", @@ -334,8 +364,281 @@ "transcoding_acceleration_vaapi": "VA-API (āĻ­āĻŋāĻĄāĻŋāĻ“ āĻ…ā§āϝāĻžāĻ•ā§āϏāĻŋāϞāĻžāϰ⧇āĻļāύ āĻāĻĒāĻŋāφāχ)", "transcoding_accepted_audio_codecs": "āĻ—ā§āϰāĻšāĻŖāϝ⧋āĻ—ā§āϝ āĻ…āĻĄāĻŋāĻ“ āϕ⧋āĻĄā§‡āĻ•āϏāĻŽā§‚āĻš (Accepted audio codecs)", "transcoding_accepted_audio_codecs_description": "āϕ⧋āύ āĻ…āĻĄāĻŋāĻ“ āϕ⧋āĻĄā§‡āĻ•āϗ⧁āϞ⧋ āĻŸā§āϰāĻžāύāϏāϕ⧋āĻĄ āĻ•āϰāĻžāϰ āĻĒā§āĻ°ā§Ÿā§‹āϜāύ āύ⧇āχ āϤāĻž āύāĻŋāĻ°ā§āĻŦāĻžāϚāύ āĻ•āϰ⧁āύāĨ¤ āĻāϟāĻŋ āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ āύāĻŋāĻ°ā§āĻĻāĻŋāĻˇā§āϟ āĻŸā§āϰāĻžāύāϏāϕ⧋āĻĄ āĻĒāϞāĻŋāϏāĻŋāϰ (transcode policies) āϜāĻ¨ā§āϝ āĻŦā§āϝāĻŦāĻšā§ƒāϤ āĻšā§ŸāĨ¤", - "transcoding_accepted_containers": "āĻ—ā§āϰāĻšāĻŖāϝ⧋āĻ—ā§āϝ āĻ•āĻ¨ā§āĻŸā§‡āχāύāĻžāϰāϏāĻŽā§‚āĻš (Accepted containers)" + "transcoding_accepted_containers": "āĻ—ā§āϰāĻšāĻŖāϝ⧋āĻ—ā§āϝ āĻ•āĻ¨ā§āĻŸā§‡āχāύāĻžāϰāϏāĻŽā§‚āĻš (Accepted containers)", + "transcoding_accepted_containers_description": "āϕ⧋āύ āĻ•āĻ¨ā§āĻŸā§‡āχāύāĻžāϰ āĻĢāϰāĻŽā§āϝāĻžāϟāϗ⧁āϞ⧋āϕ⧇ MP4-āĻ āϰāĻŋāĻŽā§āĻ•ā§āϏ āĻ•āϰāĻžāϰ āĻĒā§āϰāϝāĻŧā§‹āϜāύ āύ⧇āχ āϤāĻž āύāĻŋāĻ°ā§āĻŦāĻžāϚāύ āĻ•āϰ⧁āύāĨ¤ āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ āύāĻŋāĻ°ā§āĻĻāĻŋāĻˇā§āϟ āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄ āĻĒāϞāĻŋāϏāĻŋāϰ āϜāĻ¨ā§āϝ āĻŦā§āϝāĻŦāĻšā§ƒāϤ āĻšāϝāĻŧāĨ¤", + "transcoding_accepted_video_codecs": "āϏāĻŽāĻ°ā§āĻĨāĻŋāϤ āĻ­āĻŋāĻĄāĻŋāĻ“ āϕ⧋āĻĄā§‡āĻ•āϗ⧁āϞ⧋", + "transcoding_accepted_video_codecs_description": "āϕ⧋āύ āĻ­āĻŋāĻĄāĻŋāĻ“ āϕ⧋āĻĄā§‡āĻ•āϗ⧁āϞ⧋ āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄ āĻ•āϰāĻžāϰ āĻĒā§āϰāϝāĻŧā§‹āϜāύ āύ⧇āχ āϤāĻž āύāĻŋāĻ°ā§āĻŦāĻžāϚāύ āĻ•āϰ⧁āύāĨ¤ āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ āύāĻŋāĻ°ā§āĻĻāĻŋāĻˇā§āϟ āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄ āύ⧀āϤāĻŋāϰ āϜāĻ¨ā§āϝ āĻŦā§āϝāĻŦāĻšā§ƒāϤ āĻšāϝāĻŧāĨ¤", + "transcoding_advanced_options_description": "āĻŦ⧇āĻļāĻŋāϰāĻ­āĻžāĻ— āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻ•āϰāĻžāϰ āĻĒā§āĻ°ā§Ÿā§‹āϜāύ āύ⧇āχ āĻāĻŽāύ āĻ…āĻĒāĻļāύāϏāĻŽā§‚āĻš", + "transcoding_audio_codec": "āĻ…āĻĄāĻŋāĻ“ āϕ⧋āĻĄā§‡āĻ•", + "transcoding_audio_codec_description": "Opus āϏāĻ°ā§āĻŦā§‹āĻšā§āϚ āĻŽāĻžāύ⧇āϰ āĻ…āĻĒāĻļāύ, āϤāĻŦ⧇ āĻĒ⧁āϰ⧋āύ⧋ āĻĄāĻŋāĻ­āĻžāχāϏ āĻŦāĻž āϏāĻĢāϟāĻ“ā§Ÿā§āϝāĻžāϰ⧇āϰ āϏāĻžāĻĨ⧇ āĻāϰ āϏāĻžāĻŽāĻžā§āϜāĻ¸ā§āϝ āĻ•āĻŽāĨ¤", + "transcoding_bitrate_description": "āϏāĻ°ā§āĻŦā§‹āĻšā§āϚ āĻŦāĻŋāϟāϰ⧇āĻŸā§‡āϰ āĻšā§‡ā§Ÿā§‡ āĻŦ⧇āĻļāĻŋ āĻŦāĻž āϏāĻŽāĻ°ā§āĻĨāĻŋāϤ āĻĢāϰāĻŽā§āϝāĻžāĻŸā§‡ āύ⧟ āĻāĻŽāύ āĻ­āĻŋāĻĄāĻŋāĻ“", + "transcoding_codecs_learn_more": "āĻāĻ–āĻžāύ⧇ āĻŦā§āϝāĻŦāĻšā§ƒāϤ āĻĒāϰāĻŋāĻ­āĻžāώāĻž āϏāĻŽā§āĻĒāĻ°ā§āϕ⧇ āφāϰāĻ“ āϜāĻžāύāϤ⧇ FFmpeg āĻĄāϕ⧁āĻŽā§‡āĻ¨ā§āĻŸā§‡āĻļāύ āĻĻ⧇āϖ⧁āύ, H.264 āϕ⧋āĻĄā§‡āĻ•, HEVC āϕ⧋āĻĄā§‡āĻ• āĻāĻŦāĻ‚ VP9 āϕ⧋āĻĄā§‡āĻ•āĨ¤", + "transcoding_constant_quality_mode": "āύāĻŋāĻ°ā§āĻĻāĻŋāĻˇā§āϟ āĻŽāĻžāύ āĻŽā§‹āĻĄ", + "transcoding_constant_quality_mode_description": "ICQ, CQP-āĻāϰ āĻšā§‡ā§Ÿā§‡ āĻ­āĻžāϞ⧋ āĻŽāĻžāύ āĻĻā§‡ā§Ÿ, āĻ•āĻŋāĻ¨ā§āϤ⧁ āϏāĻŦ āĻšāĻžāĻ°ā§āĻĄāĻ“ā§Ÿā§āϝāĻžāϰ āĻ…ā§āϝāĻžāĻ•ā§āϏ⧇āϞāĻžāϰ⧇āĻļāύ āĻĄāĻŋāĻ­āĻžāχāϏ⧇ āĻ•āĻžāϜ āĻ•āϰ⧇ āύāĻžāĨ¤ āĻāχ āĻ…āĻĒāĻļāύ āϚāĻžāϞ⧁ āĻĨāĻžāĻ•āϞ⧇ āĻ•ā§‹ā§ŸāĻžāϞāĻŋāϟāĻŋ-āĻ­āĻŋāĻ¤ā§āϤāĻŋāĻ• āĻāύāϕ⧋āĻĄāĻŋāĻ‚ā§Ÿā§‡ āĻāϟāĻŋ āĻĒā§āϰāĻžāϧāĻžāĻ¨ā§āϝ āĻĒāĻžāĻŦ⧇āĨ¤ NVENC āĻāϟāĻŋ āϏāĻŽāĻ°ā§āĻĨāύ āĻ•āϰ⧇ āύāĻž, āϤāĻžāχ āĻāϟāĻŋ āωāĻĒ⧇āĻ•ā§āώāĻž āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤", + "transcoding_constant_rate_factor": "āύāĻŋāĻ°ā§āĻĻāĻŋāĻˇā§āϟ āϰ⧇āϟ āĻĢā§āϝāĻžāĻ•ā§āϟāϰ (-crf)", + "transcoding_constant_rate_factor_description": "āĻ­āĻŋāĻĄāĻŋāĻ“āϰ āϗ⧁āĻŖāĻŽāĻžāύ⧇āϰ āĻ¸ā§āϤāϰāĨ¤ āϏāĻžāϧāĻžāϰāĻŖ āĻŽāĻžāύāϗ⧁āϞ⧋ āĻšāϞ⧋ H.264-āĻāϰ āϜāĻ¨ā§āϝ ā§¨ā§Š, HEVC-āĻāϰ āϜāĻ¨ā§āϝ ā§¨ā§Ž, VP9-āĻāϰ āϜāĻ¨ā§āϝ ā§Šā§§ āĻāĻŦāĻ‚ AV1-āĻāϰ āϜāĻ¨ā§āϝ ā§Šā§ĢāĨ¤ āĻŽāĻžāύ āϝāϤ āĻ•āĻŽ āĻšāĻŦ⧇, āĻ­āĻŋāĻĄāĻŋāĻ“āϰ āϗ⧁āĻŖāĻŽāĻžāύ āϤāϤ āωāĻ¨ā§āύāϤ āĻšāĻŦ⧇, āϤāĻŦ⧇ āĻĢāĻžāχāϞ⧇āϰ āφāĻ•āĻžāϰ āϤāϤ āĻŦ⧜ āĻšāĻŦ⧇āĨ¤", + "transcoding_disabled_description": "āϕ⧋āύ⧋ āĻ­āĻŋāĻĄāĻŋāĻ“ āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄ āĻ•āϰāĻŦ⧇āύ āύāĻž, āĻāϤ⧇ āĻ•āĻŋāϛ⧁ āĻ•ā§āϞāĻžāϝāĻŧ⧇āĻ¨ā§āĻŸā§‡ āĻĒā§āϞ⧇āĻŦā§āϝāĻžāĻ• āύāĻˇā§āϟ āĻšāϤ⧇ āĻĒāĻžāϰ⧇", + "transcoding_encoding_options": "āĻāύāϕ⧋āĻĄāĻŋāĻ‚ āĻāϰ āĻ…āĻĒāĻļāύāϗ⧁āϞāĻŋ", + "transcoding_encoding_options_description": "āĻāύāϕ⧋āĻĄ āĻ•āϰāĻž āĻ­āĻŋāĻĄāĻŋāĻ“āϗ⧁āϞāĻŋāϰ āϜāĻ¨ā§āϝ āϕ⧋āĻĄā§‡āĻ•, āϰ⧇āĻœā§‹āϞāĻŋāωāĻļāύ, āϕ⧋āϝāĻŧāĻžāϞāĻŋāϟāĻŋ āĻāĻŦāĻ‚ āĻ…āĻ¨ā§āϝāĻžāĻ¨ā§āϝ āĻ…āĻĒāĻļāύ āϏ⧇āϟ āĻ•āϰ⧁āύ", + "transcoding_hardware_acceleration": "āĻšāĻžāĻ°ā§āĻĄāĻ“ā§Ÿā§āϝāĻžāϰ āĻāĻ•ā§āϏāĻŋāϞāĻžāϰ⧇āϏāύ (Acceleration)", + "transcoding_hardware_acceleration_description": "āĻĒāϰ⧀āĻ•ā§āώāĻžāĻŽā§‚āϞāĻ•: āĻĻā§āϰ⧁āϤāϤāϰ āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄāĻŋāĻ‚, āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻāĻ•āχ āĻŦāĻŋāϟāϰ⧇āĻŸā§‡ āϗ⧁āĻŖāĻŽāĻžāύ āĻšā§āϰāĻžāϏ āĻĒ⧇āϤ⧇ āĻĒāĻžāϰ⧇", + "transcoding_hardware_decoding": "āĻšāĻžāĻ°ā§āĻĄāĻ“ā§Ÿā§āϝāĻžāϰ āĻĄāĻŋāϕ⧋āĻĄāĻŋāĻ‚", + "transcoding_hardware_decoding_setting_description": "āĻļ⧁āϧ⧁ āĻāύāϕ⧋āĻĄāĻŋāĻ‚ āĻ…ā§āϝāĻžāĻ•ā§āϏāĻŋāϞāĻžāϰ⧇āĻļāύ āĻ•āϰāĻžāϰ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤ⧇ āĻāϟāĻŋ āĻāĻ¨ā§āĻĄ-āϟ⧁-āĻāĻ¨ā§āĻĄ āĻ…ā§āϝāĻžāĻ•ā§āϏāĻŋāϞāĻžāϰ⧇āĻļāύ āϏāĻ•ā§āώāĻŽ āĻ•āϰ⧇āĨ¤ āϏāĻŦ āĻ­āĻŋāĻĄāĻŋāĻ“āϤ⧇ āĻ•āĻžāϜ āύāĻžāĻ“ āĻ•āϰāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", + "transcoding_max_b_frames": "āϏāĻ°ā§āĻŦā§‹āĻšā§āϚ āĻŦāĻŋ-āĻĢā§āϰ⧇āĻŽ (B-frames)", + "transcoding_max_b_frames_description": "āĻŽāĻžāύ āϝāϤ āĻŦ⧇āĻļāĻŋ āĻšāĻŦ⧇, āĻ•āĻŽāĻĒā§āϰ⧇āĻļāύ āϤāϤ āĻ­āĻžāϞ⧋ āĻšāĻŦ⧇ āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻāύāϕ⧋āĻĄāĻŋāĻ‚ āϧ⧀āϰ⧇ āϚāϞāĻŦ⧇āĨ¤ āĻĒ⧁āϰ⧋āύ⧋ āĻĄāĻŋāĻ­āĻžāχāϏ⧇ āĻšāĻžāĻ°ā§āĻĄāĻ“ā§Ÿā§āϝāĻžāϰ āĻ…ā§āϝāĻžāĻ•ā§āϏ⧇āϞāĻžāϰ⧇āĻļāύ āĻ•āĻžāϜ āύāĻžāĻ“ āĻ•āϰāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤ ā§Ļ āĻĻāĻŋāϞ⧇ B-frames āĻŦāĻ¨ā§āϧ āĻĨāĻžāĻ•āĻŦ⧇, -ā§§ āĻĻāĻŋāϞ⧇ āĻāϟāĻŋ āύāĻŋāĻœā§‡ āĻĨ⧇āϕ⧇āχ āĻ āĻŋāĻ• āĻšāĻŦ⧇āĨ¤", + "transcoding_max_bitrate": "āϏāĻ°ā§āĻŦā§‹āĻšā§āϚ āĻŦāĻŋāϟāϰ⧇āϟ", + "transcoding_max_bitrate_description": "āϏāĻ°ā§āĻŦā§‹āĻšā§āϚ āĻŦāĻŋāϟāϰ⧇āϟ āύāĻŋāĻ°ā§āϧāĻžāϰāĻŖ āĻ•āϰāϞ⧇ āĻĢāĻžāχāϞ⧇āϰ āφāĻ•āĻžāϰ āφāϰāĻ“ āĻ…āύ⧁āĻŽāĻžāύāϝ⧋āĻ—ā§āϝ āĻšāϤ⧇ āĻĒāĻžāϰ⧇, āϤāĻŦ⧇ āĻāϰ āĻĢāϞ⧇ āϕ⧋āϝāĻŧāĻžāϞāĻŋāϟāĻŋāϰ āĻ•āĻŋāϛ⧁āϟāĻž āĻ…āĻŦāύāϤāĻŋ āϘāĻŸā§‡āĨ¤ 720p-āϤ⧇, VP9 āĻŦāĻž HEVC-āĻāϰ āϜāĻ¨ā§āϝ āϏāĻžāϧāĻžāϰāĻŖ āĻŽāĻžāύ āĻšāϞ⧋ 2600 kbit/s, āĻ…āĻĨāĻŦāĻž H.264-āĻāϰ āϜāĻ¨ā§āϝ 4500 kbit/sāĨ¤āĻāϰ āĻŽāĻžāύ 0 āϏ⧇āϟ āĻ•āϰāĻž āĻšāϞ⧇ āĻāϟāĻŋ āĻŦāĻ¨ā§āϧ āĻĨāĻžāϕ⧇āĨ¤ āϝāĻ–āύ āϕ⧋āύ⧋ āĻāĻ•āĻ• āύāĻŋāĻ°ā§āĻĻāĻŋāĻˇā§āϟ āĻ•āϰāĻž āĻĨāĻžāϕ⧇ āύāĻž, āϤāĻ–āύ k (kbit/s-āĻāϰ āϜāĻ¨ā§āϝ) āϧāϰ⧇ āύ⧇āĻ“āϝāĻŧāĻž āĻšāϝāĻŧ; āϤāĻžāχ 5000, 5000k, āĻāĻŦāĻ‚ 5M (Mbit/s-āĻāϰ āϜāĻ¨ā§āϝ) āϏāĻŽāϤ⧁āĻ˛ā§āϝāĨ¤", + "transcoding_max_keyframe_interval": "āϏāĻ°ā§āĻŦā§‹āĻšā§āϚ āϕ⧀āĻĢā§āϰ⧇āĻŽ āĻŦā§āϝāĻŦāϧāĻžāύ", + "transcoding_max_keyframe_interval_description": "āϕ⧀āĻĢā§āϰ⧇āĻŽā§‡āϰ āĻŽāĻ§ā§āϝ⧇ āϏāĻ°ā§āĻŦā§‹āĻšā§āϚ āĻĢā§āϰ⧇āĻŽ āĻĻā§‚āϰāĻ¤ā§āĻŦ āύāĻŋāĻ°ā§āϧāĻžāϰāĻŖ āĻ•āϰ⧇āĨ¤ āĻŽāĻžāύ āĻ•āĻŽ āĻšāϞ⧇ āĻ•āĻŽāĻĒā§āϰ⧇āĻļāύ āĻĻāĻ•ā§āώāϤāĻž āĻ•āĻŽā§‡, āϤāĻŦ⧇ āĻ­āĻŋāĻĄāĻŋāĻ“āϤ⧇ āϖ⧁āρāĻœā§‡ āĻŦ⧇āϰ āĻ•āϰāĻž āĻĻā§āϰ⧁āϤ āĻšā§Ÿ āĻāĻŦāĻ‚ āĻĻā§āϰ⧁āϤ āϚāϞāĻŽāĻžāύ āĻĻ⧃āĻļā§āϝ⧇ āĻŽāĻžāύāĻ“ āĻ•āĻŋāϛ⧁āϟāĻž āĻ­āĻžāϞ⧋ āĻšāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤ ā§Ļ āĻĻāĻŋāϞ⧇ āĻāχ āĻŽāĻžāύ āĻ¸ā§āĻŦ⧟āĻ‚āĻ•ā§āϰāĻŋ⧟āĻ­āĻžāĻŦ⧇ āύāĻŋāĻ°ā§āϧāĻžāϰāĻŋāϤ āĻšā§ŸāĨ¤", + "transcoding_optimal_description": "āύāĻŋāĻ°ā§āĻĻāĻŋāĻˇā§āϟ āϰ⧇āĻœā§‹āϞāĻŋāωāĻļāύ⧇āϰ āĻšā§‡ā§Ÿā§‡ āĻŦ⧜ āĻŦāĻž āϏāĻŽāĻ°ā§āĻĨāĻŋāϤ āĻĢāϰāĻŽā§āϝāĻžāĻŸā§‡ āύ⧟ āĻāĻŽāύ āĻ­āĻŋāĻĄāĻŋāĻ“", + "transcoding_policy": "āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄ āύ⧀āϤāĻŋ", + "transcoding_policy_description": "āĻ­āĻŋāĻĄāĻŋāĻ“ āĻ•āĻ–āύ āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄ āĻ•āϰāĻž āĻšāĻŦ⧇ āϤāĻž āϏ⧇āϟ āĻ•āϰ⧁āύ", + "transcoding_preferred_hardware_device": "āĻĒāĻ›āĻ¨ā§āĻĻ⧇āϰ āĻšāĻžāĻ°ā§āĻĄāĻ“āϝāĻŧā§āϝāĻžāϰ āĻĄāĻŋāĻ­āĻžāχāϏ", + "transcoding_preferred_hardware_device_description": "āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ VAAPI āĻāĻŦāĻ‚ QSV-āĻāϰ āĻ•ā§āώ⧇āĻ¤ā§āϰ⧇ āĻĒā§āϰāϝ⧋āĻœā§āϝāĨ¤ āĻšāĻžāĻ°ā§āĻĄāĻ“āϝāĻŧā§āϝāĻžāϰ āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄāĻŋāĻ‚āϝāĻŧ⧇āϰ āϜāĻ¨ā§āϝ āĻŦā§āϝāĻŦāĻšā§ƒāϤ dri āύ⧋āĻĄ āύāĻŋāĻ°ā§āϧāĻžāϰāĻŖ āĻ•āϰ⧇āĨ¤", + "transcoding_preset_preset": "āĻĒā§āϰāĻŋāϏ⧇āϟ (-preset)", + "transcoding_preset_preset_description": "āĻ•āĻŽā§āĻĒā§āϰ⧇āĻļāύ āĻ¸ā§āĻĒāĻŋāĻĄāĨ¤ āϧ⧀āϰāĻ—āϤāĻŋāϰ āĻĒā§āϰāĻŋāϏ⧇āϟāϗ⧁āϞ⧋ āϛ⧋āϟ āĻĢāĻžāχāϞ āϤ⧈āϰāĻŋ āĻ•āϰ⧇ āĻāĻŦāĻ‚ āĻāĻ•āϟāĻŋ āύāĻŋāĻ°ā§āĻĻāĻŋāĻˇā§āϟ āĻŦāĻŋāϟāϰ⧇āϟ āϞāĻ•ā§āĻˇā§āϝ āĻ•āϰāĻžāϰ āϏāĻŽāϝāĻŧ āϗ⧁āĻŖāĻŽāĻžāύ āĻŦ⧃āĻĻā§āϧāĻŋ āĻ•āϰ⧇āĨ¤ VP9 'faster'-āĻāϰ āĻšā§‡āϝāĻŧ⧇ āĻŦ⧇āĻļāĻŋ āĻ—āϤāĻŋ āωāĻĒ⧇āĻ•ā§āώāĻž āĻ•āϰ⧇āĨ¤", + "transcoding_reference_frames": "āϰ⧇āĻĢāĻžāϰ⧇āĻ¨ā§āϏ āĻĢā§āϰ⧇āĻŽ", + "transcoding_reference_frames_description": "āĻāĻ•āϟāĻŋ āĻĢā§āϰ⧇āĻŽ āĻ•āĻŽā§āĻĒā§āϰ⧇āϏ āĻ•āϰāĻžāϰ āϏāĻŽā§Ÿ āĻ•āϤāϟāĻŋ āĻĢā§āϰ⧇āĻŽāϕ⧇ āϰ⧇āĻĢāĻžāϰ⧇āĻ¨ā§āϏ āĻšāĻŋāϏ⧇āĻŦ⧇ āύ⧇āĻ“ā§ŸāĻž āĻšāĻŦ⧇āĨ¤ āĻŽāĻžāύ āϝāϤ āĻŦ⧇āĻļāĻŋ āĻšāĻŦ⧇, āĻ•āĻŽāĻĒā§āϰ⧇āĻļāύ āĻĻāĻ•ā§āώāϤāĻž āϤāϤ āĻ­āĻžāϞ⧋ āĻšāĻŦ⧇, āϤāĻŦ⧇ āĻāύāϕ⧋āĻĄāĻŋāĻ‚ āϧ⧀āϰ āĻšāĻŦ⧇āĨ¤ ā§Ļ āĻĻāĻŋāϞ⧇ āĻāχ āĻŽāĻžāύ āĻ¸ā§āĻŦ⧟āĻ‚āĻ•ā§āϰāĻŋ⧟āĻ­āĻžāĻŦ⧇ āύāĻŋāĻ°ā§āϧāĻžāϰāĻŋāϤ āĻšāĻŦ⧇āĨ¤", + "transcoding_required_description": "āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ āĻ…āύ⧁āĻŽā§‹āĻĻāĻŋāϤ āĻĢāϰāĻŽā§āϝāĻžāĻŸā§‡ āύ⧇āχ āĻāĻŽāύ āĻ­āĻŋāĻĄāĻŋāĻ“", + "transcoding_settings": "āĻ­āĻŋāĻĄāĻŋāĻ“ āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄāĻŋāĻ‚ āϏ⧇āϟāĻŋāĻ‚āϏ", + "transcoding_settings_description": "āύāĻŋāĻ°ā§āϧāĻžāϰāĻŖ āĻ•āϰ⧁āύ āϕ⧋āύ āĻ­āĻŋāĻĄāĻŋāĻ“āϗ⧁āϞ⧋āϕ⧇ āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄ āĻ•āϰāϤ⧇ āĻšāĻŦ⧇ āĻāĻŦāĻ‚ āĻ•āĻŋāĻ­āĻžāĻŦ⧇ āĻĒā§āϰāĻ•ā§āϰāĻŋ⧟āĻž āĻ•āϰāϤ⧇ āĻšāĻŦ⧇", + "transcoding_target_resolution": "āϟāĻžāĻ°ā§āϗ⧇āϟ āϰ⧇āĻœā§‹āϞāĻŋāωāĻļāύ", + "transcoding_target_resolution_description": "āωāĻšā§āϚ āϰ⧇āĻœā§‹āϞāĻŋāωāĻļāύ āĻŦ⧇āĻļāĻŋ āĻŦāĻŋāĻ¸ā§āϤāĻžāϰāĻŋāϤ āϰāĻžāϖ⧇, āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻāύāϕ⧋āĻĄāĻŋāĻ‚ āϧ⧀āϰ⧇ āĻšā§Ÿ, āĻĢāĻžāχāϞ āĻŦ⧜ āĻšā§Ÿ, āĻāĻŦāĻ‚ āĻ…ā§āϝāĻžāĻĒ āϧ⧀āϰ āĻĒā§āϰāϤāĻŋāĻ•ā§āϰāĻŋ⧟āĻž āĻ•āϰāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", + "transcoding_temporal_aq": "āĻŸā§‡āĻŽā§āĻĒā§‹āϰāĻžāϞ AQ", + "transcoding_temporal_aq_description": "āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ NVENC-āĻāϰ āĻ•ā§āώ⧇āĻ¤ā§āϰ⧇ āĻĒā§āϰāϝ⧋āĻœā§āϝāĨ¤ āĻŸā§‡āĻŽā§āĻĒā§‹āϰāĻžāϞ āĻ…ā§āϝāĻžāĻĄāĻžāĻĒāϟāĻŋāĻ­ āϕ⧋āϝāĻŧāĻžāĻ¨ā§āϟāĻžāχāĻœā§‡āĻļāύ (Adaptive Quantization) āωāĻšā§āϚ-āĻŦāĻŋāĻ¸ā§āϤāĻžāϰāĻŋāϤ āĻ“ āĻ¸ā§āĻŦāĻ˛ā§āĻĒ-āĻ—āϤāĻŋāϰ āĻĻ⧃āĻļā§āϝ⧇āϰ āĻŽāĻžāύ āĻŦ⧃āĻĻā§āϧāĻŋ āĻ•āϰ⧇āĨ¤ āĻĒ⧁āϰ⧋āύ⧋ āĻĄāĻŋāĻ­āĻžāχāϏāϗ⧁āϞ⧋āϰ āϏāĻžāĻĨ⧇ āϏāĻžāĻŽāĻžā§āϜāĻ¸ā§āϝāĻĒā§‚āĻ°ā§āĻŖ āύāĻžāĻ“ āĻšāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", + "transcoding_threads": "āĻĨā§āϰ⧇āĻĄ", + "transcoding_threads_description": "āωāĻšā§āϚ āĻŽāĻžāύ⧇ āĻāύāϕ⧋āĻĄāĻŋāĻ‚ āĻĻā§āϰ⧁āϤ āĻšā§Ÿ, āĻ•āĻŋāĻ¨ā§āϤ⧁ āϏāĻžāĻ°ā§āĻ­āĻžāϰ āĻ•āĻŽ āĻ•āĻžāϜ āĻ•āϰāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤ CPU āϕ⧋āϰ⧇āϰ āĻŦ⧇āĻļāĻŋ āĻŽāĻžāύ āĻĻ⧇āĻ“ā§ŸāĻž āωāϚāĻŋāϤ āύ⧟āĨ¤ ā§Ļ āĻĻāĻŋāϞ⧇ āϏāĻ°ā§āĻŦāĻžāϧāĻŋāĻ• āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻšāĻŦ⧇āĨ¤", + "transcoding_tone_mapping": "āĻŸā§‹āύ-āĻŽā§āϝāĻžāĻĒāĻŋāĻ‚", + "transcoding_tone_mapping_description": "āĻāχāϚāĻĄāĻŋāφāϰ (HDR) āĻ­āĻŋāĻĄāĻŋāĻ“āϕ⧇ āĻāϏāĻĄāĻŋāφāϰ (SDR)-āĻ āϰ⧂āĻĒāĻžāĻ¨ā§āϤāϰ āĻ•āϰāĻžāϰ āϏāĻŽāϝāĻŧ āĻāϰ āĻŦāĻžāĻšā§āϝāĻŋāĻ• āϰ⧂āĻĒ āĻ…āĻ•ā§āώ⧁āĻŖā§āĻŖ āϰāĻžāĻ–āĻžāϰ āĻšā§‡āĻˇā§āϟāĻž āĻ•āϰāĻž āĻšāϝāĻŧāĨ¤ āĻĒā§āϰāϤāĻŋāϟāĻŋ āĻ…ā§āϝāĻžāϞāĻ—āϰāĻŋāĻĻāĻŽ āϰāĻ™, āĻĄāĻŋāĻŸā§‡āχāϞ āĻāĻŦāĻ‚ āωāĻœā§āĻœā§āĻŦāϞāϤāĻžāϰ āϜāĻ¨ā§āϝ āĻ­āĻŋāĻ¨ā§āύ āĻ­āĻŋāĻ¨ā§āύ āϏāĻŽāĻ¨ā§āĻŦāϝāĻŧ āĻ•āϰ⧇āĨ¤ āĻšā§‡āĻŦāϞ āĻĄāĻŋāĻŸā§‡āχāϞ, āĻŽā§‹āĻŦāĻŋāϝāĻŧāĻžāϏ āϰāĻ™ āĻāĻŦāĻ‚ āϰāĻžāχāύāĻšāĻžāĻ°ā§āĻĄ āωāĻœā§āĻœā§āĻŦāϞāϤāĻž āĻ…āĻ•ā§āώ⧁āĻŖā§āĻŖ āϰāĻžāϖ⧇āĨ¤", + "transcoding_transcode_policy": "āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄ āύ⧀āϤāĻŋ", + "transcoding_transcode_policy_description": "āĻ•āĻ–āύ āĻāĻ•āϟāĻŋ āĻ­āĻŋāĻĄāĻŋāĻ“ āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄ āĻ•āϰāĻž āĻšāĻŦ⧇ āϤāĻžāϰ āύ⧀āϤāĻŋāĻŽāĻžāϞāĻžāĨ¤ HDR āĻ­āĻŋāĻĄāĻŋāĻ“ āĻāĻŦāĻ‚ YUV 4:2:0 āĻŦā§āϝāϤ⧀āϤ āĻ…āĻ¨ā§āϝ āĻĒāĻŋāĻ•ā§āϏ⧇āϞ āĻĢāϰāĻŽā§āϝāĻžāĻŸā§‡āϰ āĻ­āĻŋāĻĄāĻŋāĻ“ āϏāĻ°ā§āĻŦāĻĻāĻž āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄ āĻ•āϰāĻž āĻšāĻŦ⧇ (āϝāĻĻāĻŋ āύāĻž āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄāĻŋāĻ‚ āĻŦāĻ¨ā§āϧ āĻ•āϰāĻž āĻĨāĻžāϕ⧇)āĨ¤", + "transcoding_two_pass_encoding": "āϟ⧁-āĻĒāĻžāϏ āĻāύāϕ⧋āĻĄāĻŋāĻ‚", + "transcoding_two_pass_encoding_setting_description": "āφāϰāĻ“ āωāĻ¨ā§āύāϤ āĻŽāĻžāύ⧇āϰ āĻāύāϕ⧋āĻĄā§‡āĻĄ āĻ­āĻŋāĻĄāĻŋāĻ“ āϤ⧈āϰāĻŋ āĻ•āϰāϤ⧇ āĻĻ⧁āχ āϧāĻžāĻĒ⧇ āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄ āĻ•āϰ⧁āύāĨ¤ āϝāĻ–āύ āϏāĻ°ā§āĻŦā§‹āĻšā§āϚ āĻŦāĻŋāϟāϰ⧇āϟ āϏāĻ•ā§āϰāĻŋāϝāĻŧ āĻ•āϰāĻž āĻšāϝāĻŧ (āϝāĻž H.264 āĻāĻŦāĻ‚ HEVC-āĻāϰ āϏāĻžāĻĨ⧇ āĻ•āĻžāϜ āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ āφāĻŦāĻļā§āϝāĻ•), āϤāĻ–āύ āĻāχ āĻŽā§‹āĻĄāϟāĻŋ āϏāĻ°ā§āĻŦā§‹āĻšā§āϚ āĻŦāĻŋāϟāϰ⧇āĻŸā§‡āϰ āωāĻĒāϰ āĻ­āĻŋāĻ¤ā§āϤāĻŋ āĻ•āϰ⧇ āĻāĻ•āϟāĻŋ āĻŦāĻŋāϟāϰ⧇āϟ āϰ⧇āĻžā§āϜ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧇ āĻāĻŦāĻ‚ CRF āωāĻĒ⧇āĻ•ā§āώāĻž āĻ•āϰ⧇āĨ¤ VP9-āĻāϰ āĻ•ā§āώ⧇āĻ¤ā§āϰ⧇, āϏāĻ°ā§āĻŦā§‹āĻšā§āϚ āĻŦāĻŋāϟāϰ⧇āϟ āύāĻŋāĻˇā§āĻ•ā§āϰāĻŋāϝāĻŧ āĻĨāĻžāĻ•āϞ⧇āĻ“ CRF āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰāĻž āϝ⧇āϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", + "transcoding_video_codec": "āĻ­āĻŋāĻĄāĻŋāĻ“ āϕ⧋āĻĄā§‡āĻ•", + "transcoding_video_codec_description": "VP9 āωāĻšā§āϚ āĻ•āĻ°ā§āĻŽāĻĻāĻ•ā§āώāϤāĻž āϏāĻŽā§āĻĒāĻ¨ā§āύ āĻāĻŦāĻ‚ āĻ“āϝāĻŧ⧇āĻŦ⧇āϰ āϏāĻžāĻĨ⧇ āϏāĻžāĻŽāĻžā§āϜāĻ¸ā§āϝāĻĒā§‚āĻ°ā§āĻŖ, āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄ āĻ•āϰāϤ⧇ āĻŦ⧇āĻļāĻŋ āϏāĻŽāϝāĻŧ āϞāĻžāϗ⧇āĨ¤ HEVC-āĻāϰ āĻ•āĻ°ā§āĻŽāĻ•ā§āώāĻŽāϤāĻžāĻ“ āĻĒā§āϰāĻžāϝāĻŧ āĻāĻ•āχ āϰāĻ•āĻŽ, āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻāϰ āĻ“āϝāĻŧ⧇āĻŦ āϏāĻžāĻŽāĻžā§āϜāĻ¸ā§āϝāϤāĻž āĻ•āĻŽāĨ¤ H.264 āĻŦā§āϝāĻžāĻĒāĻ•āĻ­āĻžāĻŦ⧇ āϏāĻžāĻŽāĻžā§āϜāĻ¸ā§āϝāĻĒā§‚āĻ°ā§āĻŖ āĻāĻŦāĻ‚ āĻĻā§āϰ⧁āϤ āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄ āĻ•āϰāĻž āϝāĻžāϝāĻŧ, āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻāϟāĻŋ āĻ…āύ⧇āĻ• āĻŦāĻĄāĻŧ āĻĢāĻžāχāϞ āϤ⧈āϰāĻŋ āĻ•āϰ⧇āĨ¤ AV1 āϏāĻŦāĻšā§‡āϝāĻŧ⧇ āĻ•āĻ°ā§āĻŽāĻĻāĻ•ā§āώ āϕ⧋āĻĄā§‡āĻ•, āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻĒ⧁āϰ⧋āύ⧋ āĻĄāĻŋāĻ­āĻžāχāϏāϗ⧁āϞ⧋āϤ⧇ āĻāϰ āϏāĻŽāĻ°ā§āĻĨāύ āύ⧇āχāĨ¤", + "trash_enabled_description": "āĻŸā§āĻ°ā§āϝāĻžāĻļ āĻĢāĻŋāϚāĻžāϰ āϚāĻžāϞ⧁ āĻ•āϰ⧁āύ", + "trash_number_of_days": "āĻĻāĻŋāύ⧇āϰ āϏāĻ‚āĻ–ā§āϝāĻž", + "trash_number_of_days_description": "āĻŸā§āĻ°ā§āϝāĻžāĻļ⧇ āĻĨāĻžāĻ•āĻž āĻ…ā§āϝāĻžāϏ⧇āϟāϗ⧁āϞ⧋ āĻ¸ā§āĻĨāĻžā§Ÿā§€āĻ­āĻžāĻŦ⧇ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻžāϰ āφāϗ⧇ āϰāĻžāĻ–āĻžāϰ āĻĻāĻŋāύ āϏāĻ‚āĻ–ā§āϝāĻž", + "trash_settings": "āĻŸā§āĻ°ā§āϝāĻžāĻļ āϏ⧇āϟāĻŋāĻ‚āϏ", + "trash_settings_description": "āĻŸā§āĻ°ā§āϝāĻžāĻļ āϏ⧇āϟāĻŋāĻ‚āϏ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ", + "unlink_all_oauth_accounts": "āϏāĻ•āϞ OAuth āĻ…ā§āϝāĻžāĻ•āĻžāωāĻ¨ā§āϟ āφāύāϞāĻŋāĻ™ā§āĻ• āĻ•āϰ⧁āύ", + "unlink_all_oauth_accounts_description": "āύāϤ⧁āύ āĻĒā§āϰ⧋āĻ­āĻžāχāĻĄāĻžāϰ⧇ āĻŽāĻžāχāĻ—ā§āϰ⧇āϟ āĻ•āϰāĻžāϰ āφāϗ⧇ āϏāĻŦ OAuth āĻ…ā§āϝāĻžāĻ•āĻžāωāĻ¨ā§āϟ āφāύāϞāĻŋāĻ™ā§āĻ• āĻ•āϰ⧁āύāĨ¤", + "unlink_all_oauth_accounts_prompt": "āφāĻĒāύāĻŋ āĻ•āĻŋ āϏāĻŦ OAuth āĻ…ā§āϝāĻžāĻ•āĻžāωāĻ¨ā§āϟ āφāύāϞāĻŋāĻ™ā§āĻ• āĻ•āϰāϤ⧇ āύāĻŋāĻļā§āϚāĻŋāϤ? āĻāϟāĻŋ āĻĒā§āϰāϤāĻŋāϟāĻŋ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰ OAuth āφāχāĻĄāĻŋ āϰāĻŋāϏ⧇āϟ āĻ•āϰ⧇ āĻĻ⧇āĻŦ⧇ āĻāĻŦāĻ‚ āĻāϟāĻŋ āφāϰ āĻĒā§‚āĻ°ā§āĻŦāĻžāĻŦāĻ¸ā§āĻĨāĻžāϝāĻŧ āĻĢ⧇āϰāĻžāύ⧋ āϝāĻžāĻŦ⧇ āύāĻžāĨ¤", + "user_cleanup_job": "āχāωāϜāĻžāϰ āĻ•ā§āϞāĻŋāύāφāĻĒ", + "user_delete_delay": "{user}-āĻāϰ āĻ…ā§āϝāĻžāĻ•āĻžāωāĻ¨ā§āϟ āĻāĻŦāĻ‚ āĻ…ā§āϝāĻžāϏ⧇āϟ {delay, plural, one {# day} other {# days}} āĻĒāϰ āĻ¸ā§āĻĨāĻžāϝāĻŧā§€āĻ­āĻžāĻŦ⧇ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻžāϰ āϜāĻ¨ā§āϝ āύāĻŋāĻ°ā§āϧāĻžāϰāĻŋāϤ āĻšāĻŦ⧇āĨ¤", + "user_delete_delay_settings": "āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻžāϰ āϏāĻŽā§Ÿ āĻŦāĻŋāϞāĻŽā§āĻŦ", + "user_delete_delay_settings_description": "āĻ…ā§āϝāĻžāĻ•āĻžāωāĻ¨ā§āϟ āĻāĻŦāĻ‚ āĻ…ā§āϝāĻžāϏ⧇āϟ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻžāϰ āĻĒāϰ āĻ•āϤ āĻĻāĻŋāύ⧇āϰ āĻŽāĻ§ā§āϝ⧇ āĻ¸ā§āĻĨāĻžā§Ÿā§€āĻ­āĻžāĻŦ⧇ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž āĻšāĻŦ⧇āĨ¤ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻžāϰ āĻ•āĻžāϜ āĻŽāĻ§ā§āϝāϰāĻžāϤ⧇ āϚāĻžāϞāĻžāύ⧋ āĻšā§Ÿ āĻāĻŦāĻ‚ āĻĻ⧇āĻ–āĻž āĻšā§Ÿ āϕ⧋āύ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āĻ¸ā§āĻĨāĻžā§Ÿā§€āĻ­āĻžāĻŦ⧇ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻžāϰ āϜāĻ¨ā§āϝ āĻĒā§āϰāĻ¸ā§āϤ⧁āϤāĨ¤ āĻāχ āϏ⧇āϟāĻŋāĻ‚ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻ•āϰāϞ⧇ āĻĒāϰāĻŦāĻ°ā§āϤ⧀ āĻāĻ•ā§āϏāĻŋāĻ•āĻŋāωāĻļāύ⧇āϰ āϏāĻŽā§Ÿ āϤāĻž āĻĒā§āϰāϝ⧋āĻœā§āϝ āĻšāĻŦ⧇āĨ¤", + "user_delete_immediately": "{user}-āĻāϰ āĻ…ā§āϝāĻžāĻ•āĻžāωāĻ¨ā§āϟ āĻāĻŦāĻ‚ āĻ…ā§āϝāĻžāϏ⧇āϟ āĻ¸ā§āĻĨāĻžāϝāĻŧā§€āĻ­āĻžāĻŦ⧇ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻžāϰ āϜāĻ¨ā§āϝ immediately āĻ•āĻŋāωāϤ⧇ āĻ…āĻ¨ā§āϤāĻ°ā§āϭ⧁āĻ•ā§āϤ āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤", + "user_delete_immediately_checkbox": "āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āĻ“ āĻ…ā§āϝāĻžāϏ⧇āϟ āĻ¤ā§ŽāĻ•ā§āώāĻŖāĻžā§Ž āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻžāϰ āϜāĻ¨ā§āϝ āĻ•āĻŋāω", + "user_details": "āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āϤāĻĨā§āϝ", + "user_management": "āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āĻŽā§āϝāĻžāύ⧇āϜāĻŽā§‡āĻ¨ā§āϟ", + "user_password_has_been_reset": "āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰ āĻĒāĻžāϏāĻ“ā§ŸāĻžāĻ°ā§āĻĄ āϰāĻŋāϏ⧇āϟ āĻ•āϰāĻž āĻšā§Ÿā§‡āϛ⧇:", + "user_password_reset_description": "āĻĻāϝāĻŧāĻž āĻ•āϰ⧇ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰ āϜāĻ¨ā§āϝ āϏāĻžāĻŽāϝāĻŧāĻŋāĻ• āĻĒāĻžāϏāĻ“ā§ŸāĻžāĻ°ā§āĻĄ āĻĻāĻŋāύ āĻāĻŦāĻ‚ āϜāĻžāύāĻŋā§Ÿā§‡ āĻĻāĻŋāύ āϝ⧇ āϤāĻžāϰāĻž āĻĒāϰāĻŦāĻ°ā§āϤ⧀ āϞāĻ—āχāύ⧇ āĻĒāĻžāϏāĻ“ā§ŸāĻžāĻ°ā§āĻĄ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻ•āϰāĻŦ⧇āύāĨ¤", + "user_restore_description": "{user} āĻāϰ āĻ…ā§āϝāĻžāĻ•āĻžāωāĻ¨ā§āϟ āĻĒ⧁āύāϰ⧁āĻĻā§āϧāĻžāϰ āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤", + "user_restore_scheduled_removal": "āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āĻĒ⧁āύāϰ⧁āĻĻā§āϧāĻžāϰ āĻ•āϰ⧁āύ - āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻžāϰ āϜāĻ¨ā§āϝ āύāĻŋāĻ°ā§āϧāĻžāϰāĻŋāϤ āϤāĻžāϰāĻŋāĻ–:{date, date, long}", + "user_settings": "āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āϏ⧇āϟāĻŋāĻ‚āϏ", + "user_settings_description": "āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āϏ⧇āϟāĻŋāĻ‚āϏ āĻŽā§āϝāĻžāύ⧇āϜ āĻ•āϰ⧁āύ", + "user_successfully_removed": "āϏāĻĢāϞāĻ­āĻžāĻŦ⧇ āχāωāϜāĻžāϰ {email}-āϕ⧇ āϏāϰāĻŋā§Ÿā§‡ āĻĻ⧇āĻ“ā§ŸāĻž āĻšā§Ÿā§‡āϛ⧇āĨ¤", + "version_check_enabled_description": "āĻ­āĻžāĻ°ā§āϏāύ āϝāĻžāϚāĻžāχ āϚāĻžāϞ⧁ āĻ•āϰ⧁āύ", + "version_check_implications": "āĻ­āĻžāĻ°ā§āϏāύ āĻšā§‡āĻ• āĻĢāĻŋāϚāĻžāϰāϟāĻŋ github.com-āĻāϰ āϏāĻ™ā§āϗ⧇ āύāĻŋ⧟āĻŽāĻŋāϤ āϏāĻ‚āϝ⧋āϗ⧇āϰ āĻ“āĻĒāϰ āύāĻŋāĻ°ā§āĻ­āϰāĻļā§€āϞ", + "version_check_settings": "āĻ­āĻžāĻ°ā§āϏāύ āϝāĻžāϚāĻžāχ", + "version_check_settings_description": "āύāϤ⧁āύ āĻ­āĻžāĻ°ā§āϏāύ⧇āϰ āύ⧋āϟāĻŋāĻĢāĻŋāϕ⧇āĻļāύ āϚāĻžāϞ⧁/āĻŦāĻ¨ā§āϧ āĻ•āϰ⧁āύ", + "video_conversion_job": "āĻ­āĻŋāĻĄāĻŋāĻ“ āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄ āĻ•āϰ⧁āύ", + "video_conversion_job_description": "āĻŦā§āϰāĻžāωāϜāĻžāϰ āĻāĻŦāĻ‚ āĻĄāĻŋāĻ­āĻžāχāϏ⧇ āφāϰāĻ“ āĻ­āĻžāϞ⧋āĻ­āĻžāĻŦ⧇ āϚāϞāĻžāϰ āϜāĻ¨ā§āϝ āĻ­āĻŋāĻĄāĻŋāĻ“ āĻŸā§āϰāĻžāĻ¨ā§āϏāϕ⧋āĻĄ āĻ•āϰ⧁āύ" }, + "admin_email": "āĻ…ā§āϝāĻžāĻĄāĻŽāĻŋāύ⧇āϰ āχāĻŽā§‡āχāϞ", + "admin_password": "āĻ…ā§āϝāĻžāĻĄāĻŽāĻŋāύ⧇āϰ āĻĒāĻžāϏāĻ“ā§ŸāĻžāĻ°ā§āĻĄ", + "administration": "āĻ…ā§āϝāĻžāĻĄāĻŽāĻŋāύ", + "advanced": "āĻ…ā§āϝāĻžāĻĄāĻ­āĻžāĻ¨ā§āϏāĻĄ", + "age_months": "āĻŦ⧟āϏ {months, plural, one {# month} other {# months}}", + "age_year_months": "āĻŦ⧟āϏ ā§§ āĻŦāĻ›āϰ, {months, plural, one {# month} other {# months}}", + "album_added": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽ āϝ⧁āĻ•ā§āϤ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "album_added_notification_setting_description": "āĻļ⧇āϝāĻŧāĻžāϰ āĻ•āϰāĻž āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡ āϝ⧁āĻ•ā§āϤ āĻšāϞ⧇ āχāĻŽā§‡āχāϞ āύ⧋āϟāĻŋāĻĢāĻŋāϕ⧇āĻļāύ āĻĒāĻžāύ", + "album_cover_updated": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡āϰ āĻ•āĻ­āĻžāϰ āφāĻĒāĻĄā§‡āϟ āĻšāϝāĻŧ⧇āϛ⧇", + "album_delete_confirmation": "āφāĻĒāύāĻŋ āĻ•āĻŋ āϏāĻ¤ā§āϝāĻŋāχ āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽ {album} āĻŽā§āϛ⧇ āĻĢ⧇āϞāϤ⧇ āϚāĻžāύ?", + "album_delete_confirmation_description": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽāϟāĻŋ āĻļā§‡ā§ŸāĻžāϰ āĻ•āϰāĻž āĻĨāĻžāĻ•āϞ⧇āĻ“ āĻ…āĻ¨ā§āϝ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰāĻž āφāϰ āĻāϟāĻŋ āĻ…ā§āϝāĻžāĻ•ā§āϏ⧇āϏ āĻ•āϰāϤ⧇ āĻĒāĻžāϰāĻŦ⧇āύ āύāĻžāĨ¤", + "album_info_updated": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡āϰ āϤāĻĨā§āϝ āφāĻĒāĻĄā§‡āϟ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "album_leave": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽ āĻĨ⧇āϕ⧇ āĻŦ⧇āϰāĻŋā§Ÿā§‡ āϝ⧇āϤ⧇ āϚāĻžāύ ?", + "album_leave_confirmation": "āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āϝ⧇ āφāĻĒāύāĻŋ {album} āϛ⧇āĻĄāĻŧ⧇ āϝ⧇āϤ⧇ āϚāĻžāύ?", + "album_name": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡āϰ āύāĻžāĻŽ", + "album_options": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡āϰ āĻ…āĻĒāĻļāύāϏāĻŽā§‚āĻš", + "album_remove_user": "āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āϏāϰāĻžāϤ⧇ āϚāĻžāύ?", + "album_remove_user_confirmation": "āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āϝ⧇ āφāĻĒāύāĻŋ {user}-āϕ⧇ āϏāϰāĻžāϤ⧇ āϚāĻžāύ?", + "album_share_no_users": "āĻāχ āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽāϟāĻŋ āϏāĻŦ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰ āϏāĻ™ā§āϗ⧇ āĻļā§‡ā§ŸāĻžāϰ āĻ•āϰāĻž āĻšā§Ÿā§‡āϛ⧇, āĻŦāĻž āĻļā§‡ā§ŸāĻžāϰ āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ āϕ⧋āύ⧋ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āύ⧇āχāĨ¤", + "album_updated": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽ āφāĻĒāĻĄā§‡āϟ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "album_updated_setting_description": "āύāϤ⧁āύ āĻ…ā§āϝāĻžāϏ⧇āϟ āϝ⧁āĻ•ā§āϤ āĻšāϞ⧇ āĻļ⧇āϝāĻŧāĻžāϰ āĻ•āϰāĻž āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡āϰ āϜāĻ¨ā§āϝ āχāĻŽā§‡āχāϞ āύ⧋āϟāĻŋāĻĢāĻŋāϕ⧇āĻļāύ āĻĒāĻžāύ", + "album_user_left": "āĻŦāĻžāĻŽ {album}", + "album_user_removed": "{user} āϕ⧇ āϏāϰāĻžāύ⧋ āĻšāϝāĻŧ⧇āϛ⧇", + "album_with_link_access": "āϞāĻŋāĻ™ā§āĻ• āĻĨāĻžāĻ•āĻž āϝ⧇ āϕ⧇āω āĻāχ āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡āϰ āĻ›āĻŦāĻŋ āĻ“ āĻŽāĻžāύ⧁āώāϜāύāϕ⧇ āĻĻ⧇āĻ–āϤ⧇ āĻĒāĻžāϰāĻŦ⧇āĨ¤", + "albums": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽāϏāĻŽā§‚āĻš", + "all": "āϏāĻŦ", + "all_albums": "āϏāĻ•āϞ āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽāϏāĻŽā§‚āĻš", + "all_people": "āϏāĻŦ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀", + "all_videos": "āϏāĻŦ āĻ­āĻŋāĻĄāĻŋāĻ“", + "allow_dark_mode": "āĻĄāĻžāĻ°ā§āĻ• āĻŽā§‹āĻĄ āϚāĻžāϞ⧁ āĻ•āϰ⧁āύ", + "allow_edits": "āĻāĻĄāĻŋāĻŸā§‡āϰ āĻ…āύ⧁āĻŽāϤāĻŋ āĻĻāĻŋāύ", + "allow_public_user_to_download": "āϏāĻžāϧāĻžāϰāĻŖ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āĻĄāĻžāωāύāϞ⧋āĻĄ āĻ•āϰāϤ⧇ āĻĒāĻžāϰāĻŦ⧇", + "allow_public_user_to_upload": "āϏāĻžāϧāĻžāϰāĻŖ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āφāĻĒāϞ⧋āĻĄ āĻ•āϰāϤ⧇ āĻĒāĻžāϰāĻŦ⧇", + "anti_clockwise": "āĻŦāĻŋāĻĒāϰ⧀āϤ āĻĻāĻŋāĻ•", + "api_key": "API āϕ⧀", + "api_key_description": "āĻāχ āĻŽāĻžāύ āĻāĻ•āĻŦāĻžāϰāχ āĻĻ⧇āĻ–āĻžāύ⧋ āĻšāĻŦ⧇āĨ¤ āωāχāĻ¨ā§āĻĄā§‹ āĻŦāĻ¨ā§āϧ āĻ•āϰāĻžāϰ āφāϗ⧇ āĻ…āĻŦāĻļā§āϝāχ āĻāϟāĻŋ āĻ•āĻĒāĻŋ āĻ•āϰ⧁āύāĨ¤", + "api_key_empty": "API āϕ⧀-āĻāϰ āύāĻžāĻŽ āĻ–āĻžāϞāĻŋ āϰāĻžāĻ–āĻž āϝāĻžāĻŦ⧇ āύāĻž", + "api_keys": "API āϕ⧀ āϏāĻŽā§‚āĻš", + "app_settings": "āĻ…ā§āϝāĻžāĻĒ āϏ⧇āϟāĻŋāĻ‚āϏ", + "appears_in": "v1.106.4 āĻĨ⧇āϕ⧇, āĻ…ā§āϝāĻžāϏ⧇āϟ āϏāĻžāχāĻĄāĻŦāĻžāϰ⧇ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻšā§Ÿ ‘[albums]-āĻ āωāĻĒāĻ¸ā§āĻĨāĻŋāĻ¤â€™ āĻŦā§‹āĻāĻžāϤ⧇", + "archive": "āφāĻ°ā§āĻ•āĻžāχāĻ­", + "archive_or_unarchive_photo": "āĻĢāĻŸā§‹ āφāĻ°ā§āĻ•āĻžāχāĻ­ āĻ…āĻĨāĻŦāĻž āφāύāφāĻ°ā§āĻ•āĻžāχāĻ­ āĻ•āϰ⧁āύ", + "archive_size": "āφāĻ°ā§āĻ•āĻžāχāĻ­ āϏāĻžāχāϜ", + "archive_size_description": "āĻĄāĻžāωāύāϞ⧋āĻĄā§‡āϰ āφāĻ°ā§āĻ•āĻžāχāĻ­ āϏāĻžāχāϜ āύāĻŋāĻ°ā§āϧāĻžāϰāĻŖ āĻ•āϰ⧁āύ (GiB)", + "are_these_the_same_person": "āĻāϰāĻž āĻ•āĻŋ āĻāĻ•āχ āĻŦā§āϝāĻ•ā§āϤāĻŋ?", + "are_you_sure_to_do_this": "āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āϝ⧇ āφāĻĒāύāĻŋ āĻāϟāĻŋ āĻ•āϰāϤ⧇ āϚāĻžāύ?", + "asset_added_to_album": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡ āϝ⧁āĻ•ā§āϤ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "asset_adding_to_album": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡ āϝ⧁āĻ•ā§āϤ āĻ•āϰāĻž āĻšāĻšā§āϛ⧇â€Ļ", + "asset_description_updated": "āĻ…ā§āϝāĻžāϏ⧇āĻŸā§‡āϰ āĻŦāĻŋāĻŦāϰāĻŖ āφāĻĒāĻĄā§‡āϟ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "asset_filename_is_offline": "{filename} āĻ…ā§āϝāĻžāϏ⧇āϟāϟāĻŋ āĻŦāĻ°ā§āϤāĻŽāĻžāύ⧇ āĻ…āĻĢāϞāĻžāχāύ", + "asset_has_unassigned_faces": "āĻ…ā§āϝāĻžāϏ⧇āϟāϟāĻŋāϰ āĻ•āĻŋāϛ⧁ āĻŽā§āĻ– āĻ…āύāĻŋāĻ°ā§āϧāĻžāϰāĻŋāϤ āĻĢ⧇āϏ āϰāϝāĻŧ⧇āϛ⧇", + "asset_hashing": "āĻšā§āϝāĻžāĻļāĻŋāĻ‚ āϚāϞāϛ⧇â€Ļ", + "asset_offline": "āĻ…ā§āϝāĻžāϏ⧇āϟ āĻŦāĻ°ā§āϤāĻŽāĻžāύ⧇ āĻ…āĻĢāϞāĻžāχāύ", + "asset_offline_description": "āĻāχ āĻāĻ•ā§āϏāϟāĻžāĻ°ā§āύāĻžāϞ āĻ…ā§āϝāĻžāϏ⧇āϟāϟāĻŋ āĻāĻ–āύ āĻĄāĻŋāĻ¸ā§āϕ⧇ āύ⧇āχāĨ¤ āϏāĻšāĻžāϝāĻŧāϤāĻžāϰ āϜāĻ¨ā§āϝ Immich āĻ…ā§āϝāĻžāĻĄāĻŽāĻŋāύāĻŋāĻ¸ā§āĻŸā§āϰ⧇āϟāϰ⧇āϰ āϏāĻžāĻĨ⧇ āϝ⧋āĻ—āĻžāϝ⧋āĻ— āĻ•āϰ⧁āύāĨ¤", + "asset_skipped": "āĻāĻĄāĻŧāĻžāύ⧋ āĻšāϝāĻŧ⧇āϛ⧇", + "asset_skipped_in_trash": "āĻŸā§āĻ°ā§āϝāĻžāĻļ⧇", + "asset_uploaded": "āφāĻĒāϞ⧋āĻĄ āϏāĻŽā§āĻĒāĻ¨ā§āύ", + "asset_uploading": "āφāĻĒāϞ⧋āĻĄ āϚāϞāϛ⧇â€Ļ", + "assets": "āĻ…ā§āϝāĻžāϏ⧇āϟāϏāĻŽā§‚āĻš", + "assets_added_to_album_count": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡ {count, plural, one {# asset} other {# assets}} āϝ⧁āĻ•ā§āϤ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "assets_moved_to_trash_count": "{count, plural, one {# asset} other {# assets}} āĻŸā§āĻ°ā§āϝāĻžāĻļ⧇ āϏāϰāĻžāύ⧋ āĻšāϝāĻŧ⧇āϛ⧇", + "assets_permanently_deleted_count": "{count, plural, one {# asset} other {# assets}} āĻ¸ā§āĻĨāĻžā§Ÿā§€āĻ­āĻžāĻŦ⧇ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "assets_removed_count": "{count, plural, one {# asset} other {# assets}} āϏāϰāĻžāύ⧋ āĻšāϝāĻŧ⧇āϛ⧇", + "assets_restore_confirmation": "āφāĻĒāύāĻŋ āĻ•āĻŋ āϏāĻ¤ā§āϝāĻŋāχ āφāĻĒāύāĻžāϰ āϏāĻŦ āĻŸā§āĻ°ā§āϝāĻžāĻļ āĻ•āϰāĻž āĻ…ā§āϝāĻžāϏ⧇āϟ āĻĒ⧁āύāϰ⧁āĻĻā§āϧāĻžāϰ āĻ•āϰāϤ⧇ āϚāĻžāύ? āĻāϟāĻŋ āĻĒā§‚āĻ°ā§āĻŦāĻžāĻŦāĻ¸ā§āĻĨāĻžāϝāĻŧ āĻĢāĻŋāϰāĻžāύ⧋ āϝāĻžāĻŦ⧇ āύāĻžāĨ¤ āϤāĻŦ⧇ āĻ…āĻĢāϞāĻžāχāύ āĻ…ā§āϝāĻžāϏ⧇āϟ āĻāχāĻ­āĻžāĻŦ⧇ āĻĒ⧁āύāϰ⧁āĻĻā§āϧāĻžāϰ āĻšāĻŦ⧇ āύāĻžāĨ¤", + "assets_restored_count": "{count, plural, one {# asset} other {# assets}} āĻĒ⧁āύāϰ⧁āĻĻā§āϧāĻžāϰ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "assets_trashed_count": "{count, plural, one {# asset} other {# assets}} āĻŸā§āĻ°ā§āϝāĻžāĻļ⧇ āĻĒāĻžāĻ āĻžāύ⧋ āĻšā§Ÿā§‡āϛ⧇", + "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} āφāϗ⧇āχ āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡ āϝ⧁āĻ•ā§āϤ āĻ›āĻŋāϞ", + "authorized_devices": "āĻ…āύ⧁āĻŽā§‹āĻĻāĻŋāϤ āĻĄāĻŋāĻ­āĻžāχāϏ", + "back": "āĻĢāĻŋāϰ⧇ āϝāĻžāύ", + "back_close_deselect": "āĻĢāĻŋāϰ⧇ āϝāĻžāύ, āĻŦāĻ¨ā§āϧ āĻ•āϰ⧁āύ āĻŦāĻž āύāĻŋāĻ°ā§āĻŦāĻžāϚāύ āĻŦāĻžāϤāĻŋāϞ āĻ•āϰ⧁āύ", + "backward": "āĻĒāĻŋāĻ›āύ⧇", + "birthdate_saved": "āϜāĻ¨ā§āĻŽ āϤāĻžāϰāĻŋāĻ– āϏāĻ‚āϰāĻ•ā§āώāĻŖ āϏāĻŽā§āĻĒāĻ¨ā§āύ", + "birthdate_set_description": "āĻāĻ•āϟāĻŋ āĻ›āĻŦāĻŋāϰ āϏāĻŽā§Ÿā§‡ āĻŦā§āϝāĻ•ā§āϤāĻŋāϰ āĻŦ⧟āϏ āĻ—āĻŖāύāĻžāϰ āϜāĻ¨ā§āϝ āϜāĻ¨ā§āĻŽ āϤāĻžāϰāĻŋāĻ– āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰāĻž āĻšā§ŸāĨ¤", + "blurred_background": "āĻŦā§āϞāĻžāϰāĻĄ āĻŦā§āϝāĻžāĻ•āĻ—ā§āϰāĻžāωāĻ¨ā§āĻĄ", + "bugs_and_feature_requests": "āĻŦāĻžāĻ— āĻ“ āĻĢāĻŋāϚāĻžāϰ āϰāĻŋāĻ•ā§‹ā§Ÿā§‡āĻ¸ā§āϟ", + "build": "āĻŦāĻŋāĻ˛ā§āĻĄ", + "build_image": "āĻŦāĻŋāĻ˛ā§āĻĄ āχāĻŽā§‡āϜ", + "bulk_delete_duplicates_confirmation": "āφāĻĒāύāĻŋ āĻ•āĻŋ āϏāĻ¤ā§āϝāĻŋāχ {count, plural, one {# duplicate asset} other {# duplicate assets}} āĻāĻ•āϏāĻžāĻĨ⧇ āĻŽā§āϛ⧇ āĻĢ⧇āϞāϤ⧇ āϚāĻžāύ? āĻĒā§āϰāϤāĻŋāϟāĻŋ āĻ—ā§āϰ⧁āĻĒ⧇āϰ āϏāĻŦāĻšā§‡ā§Ÿā§‡ āĻŦ⧜ āĻ…ā§āϝāĻžāϏ⧇āϟ āϰāĻžāĻ–āĻž āĻšāĻŦ⧇, āĻŦāĻžāĻ•āĻŋāϗ⧁āϞ⧋ āĻ¸ā§āĻĨāĻžā§Ÿā§€āĻ­āĻžāĻŦ⧇ āĻŽā§āϛ⧇ āϝāĻžāĻŦ⧇āĨ¤ āĻāϟāĻŋ āĻĒā§‚āĻ°ā§āĻŦāĻžāĻŦāĻ¸ā§āĻĨāĻžāϝāĻŧ āĻĢāĻŋāϰāĻžāύ⧋ āϝāĻžāĻŦ⧇ āύāĻž!", + "bulk_keep_duplicates_confirmation": "āφāĻĒāύāĻŋ āĻ•āĻŋ āϏāĻ¤ā§āϝāĻŋāχ {count, plural, one {# duplicate asset} other {# duplicate assets}} āϰāĻžāĻ–āϤ⧇ āϚāĻžāύ? āϏāĻŦ āĻĄā§āĻĒā§āϞāĻŋāϕ⧇āϟ āĻ—ā§āϰ⧁āĻĒ āĻ āĻŋāĻ• āĻ•āϰāĻž āĻšāĻŦ⧇, āϕ⧋āύ⧋ āĻ•āĻŋāϛ⧁ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž āĻšāĻŦ⧇ āύāĻžāĨ¤", + "bulk_trash_duplicates_confirmation": "āφāĻĒāύāĻŋ āĻ•āĻŋ āϏāĻ¤ā§āϝāĻŋāχ {count, plural, one {# duplicate asset} other {# duplicate assets}} āĻāĻ•āϏāĻžāĻĨ⧇ āĻŸā§āĻ°ā§āϝāĻžāĻļ āĻ•āϰāϤ⧇ āϚāĻžāύ? āĻĒā§āϰāϤāĻŋāϟāĻŋ āĻ—ā§āϰ⧁āĻĒ⧇āϰ āϏāĻŦāĻšā§‡ā§Ÿā§‡ āĻŦ⧜ āĻ…ā§āϝāĻžāϏ⧇āϟ āϰāĻžāĻ–āĻž āĻšāĻŦ⧇, āĻŦāĻžāĻ•āĻŋāϗ⧁āϞ⧋ āĻŸā§āĻ°ā§āϝāĻžāĻļ⧇ āϝāĻžāĻŦ⧇āĨ¤", + "buy": "Immich āĻ•ā§āϰ⧟ āĻ•āϰ⧁āύ", + "camera": "āĻ•ā§āϝāĻžāĻŽā§‡āϰāĻž", + "camera_brand": "āĻ•ā§āϝāĻžāĻŽā§‡āϰāĻž āĻŦā§āĻ°ā§āϝāĻžāĻ¨ā§āĻĄ", + "camera_model": "āĻ•ā§āϝāĻžāĻŽā§‡āϰāĻž āĻŽāĻĄā§‡āϞ", + "cancel": "āĻŦāĻžāϤāĻŋāϞ", + "cancel_search": "āϏāĻžāĻ°ā§āϚ āĻŦāĻ¨ā§āϧ āĻ•āϰ⧁āύ", + "cannot_merge_people": "āĻŦā§āϝāĻ•ā§āϤāĻŋāĻĻ⧇āϰ āĻāĻ•āĻ¤ā§āϰ āĻ•āϰāĻž āϏāĻŽā§āĻ­āĻŦ āύāϝāĻŧ", + "cannot_undo_this_action": "āĻāχ āĻ•āĻžāϜ āĻĒā§‚āĻ°ā§āĻŦāĻžāĻŦāĻ¸ā§āĻĨāĻžāϝāĻŧ āĻĢ⧇āϰāĻžāύ⧋ āϝāĻžāĻŦ⧇ āύāĻž!", + "cannot_update_the_description": "āĻŦāĻŋāĻŦāϰāĻŖ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āϏāĻŽā§āĻ­āĻŦ āύāϝāĻŧ", + "change_date": "āϤāĻžāϰāĻŋāĻ– āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ", + "change_expiration_time": "āĻŽā§‡āϝāĻŧāĻžāĻĻ āĻļ⧇āώ⧇āϰ āϏāĻŽāϝāĻŧ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ", + "change_location": "āϞ⧋āϕ⧇āĻļāύ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ", + "change_name": "āύāĻžāĻŽ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻ•āϰ⧁āύ", + "change_name_successfully": "āύāĻžāĻŽ āϏāĻĢāϞāĻ­āĻžāĻŦ⧇ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻšāϝāĻŧ⧇āϛ⧇", + "change_password": "āĻĒāĻžāϏāĻ“ā§ŸāĻžāĻ°ā§āĻĄ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻ•āϰ⧁āύ", + "change_password_description": "āφāĻĒāύāĻŋ āĻšāϝāĻŧāϤ⧋ āĻĒā§āϰāĻĨāĻŽāĻŦāĻžāϰ āϞāĻ—āχāύ āĻ•āϰāϛ⧇āύ āĻŦāĻž āĻĒāĻžāϏāĻ“ā§ŸāĻžāĻ°ā§āĻĄ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ⧇āϰ āĻ…āύ⧁āϰ⧋āϧ āĻ•āϰ⧇āϛ⧇āύāĨ¤ āύāĻŋāĻšā§‡ āύāϤ⧁āύ āĻĒāĻžāϏāĻ“ā§ŸāĻžāĻ°ā§āĻĄ āĻĻāĻŋāύāĨ¤", + "change_your_password": "āφāĻĒāύāĻžāϰ āĻĒāĻžāϏāĻ“ā§ŸāĻžāĻ°ā§āĻĄ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻ•āϰ⧁āύ", + "changed_visibility_successfully": "āĻ­āĻŋāϏāĻŋāĻŦāĻŋāϞāĻŋāϟāĻŋ āϏāĻĢāϞāĻ­āĻžāĻŦ⧇ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻšāϝāĻŧ⧇āϛ⧇", + "check_logs": "āϞāĻ— āĻĻ⧇āϖ⧁āύ", + "choose_matching_people_to_merge": "āĻāĻ•āĻ¤ā§āϰ āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ āĻŽāĻŋāϞ āĻĨāĻžāĻ•āĻž āĻŦā§āϝāĻ•ā§āϤāĻŋāĻĻ⧇āϰ āύāĻŋāĻ°ā§āĻŦāĻžāϚāύ āĻ•āϰ⧁āύ", + "city": "āĻļāĻšāϰ", + "clear": "āĻŽā§āϛ⧁āύ", + "clear_all": "āϏāĻŦ āĻŽā§āϛ⧁āύ", + "clear_all_recent_searches": "āϏāĻžāĻŽā§āĻĒā§āϰāϤāĻŋāĻ• āϏāĻŦ āĻ…āύ⧁āϏāĻ¨ā§āϧāĻžāύ āĻĒāϰāĻŋāĻˇā§āĻ•āĻžāϰ āĻ•āϰ⧁āύ", + "clear_message": "āĻŽā§‡āϏ⧇āϜ āĻĒāϰāĻŋāĻˇā§āĻ•āĻžāϰ āĻ•āϰ⧁āύ", + "clear_value": "āĻ­ā§āϝāĻžāϞ⧁ āĻŽā§āϛ⧁āύ", + "clockwise": "āϘ⧜āĻŋāϰ āĻ•āĻžāρāϟāĻžāϰ āĻĻāĻŋāϕ⧇", + "close": "āĻŦāĻ¨ā§āϧ", + "collapse": "āϏāĻ‚āϕ⧁āϚāĻŋāϤ āĻ•āϰ⧁āύ", + "collapse_all": "āϏāĻŦ āϏāĻ‚āϕ⧁āϚāĻŋāϤ", + "color": "āϰāĻ‚", + "color_theme": "āĻ•āĻžāϞāĻžāϰ āĻĨāĻŋāĻŽ", + "comment_deleted": "āĻŽāĻ¨ā§āϤāĻŦā§āϝ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "comment_options": "āĻŽāĻ¨ā§āϤāĻŦā§āϝ āĻ…āĻĒāĻļāύ", + "comments_and_likes": "āĻŽāĻ¨ā§āϤāĻŦā§āϝ āĻ“ āϞāĻžāχāĻ•", + "comments_are_disabled": "āĻŽāĻ¨ā§āϤāĻŦā§āϝ āĻŦāĻ¨ā§āϧ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "confirm": "āύāĻŋāĻļā§āϚāĻŋāϤ", + "confirm_admin_password": "āĻ…ā§āϝāĻžāĻĄāĻŽāĻŋāύ āĻĒāĻžāϏāĻ“ā§ŸāĻžāĻ°ā§āĻĄ āĻĒ⧁āύāϰāĻžā§Ÿ āϞāĻŋāϖ⧁āύ", + "confirm_delete_shared_link": "āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āϝ⧇ āφāĻĒāύāĻŋ āĻāχ āĻļ⧇āϝāĻŧāĻžāϰ āĻ•āϰāĻž āϞāĻŋāĻ™ā§āĻ•āϟāĻŋ āĻŽā§āϛ⧇ āĻĢ⧇āϞāϤ⧇ āϚāĻžāύ?", + "confirm_keep_this_delete_others": "āĻ¸ā§āĻŸā§āϝāĻžāϕ⧇āϰ āĻāχ āĻ…ā§āϝāĻžāϏ⧇āϟ āĻ›āĻžā§œāĻž āϏāĻŦ āĻ…āĻ¨ā§āϝāĻžāĻ¨ā§āϝ āĻ…ā§āϝāĻžāϏ⧇āϟ āĻŽā§āϛ⧇ āϝāĻžāĻŦ⧇āĨ¤ āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āϝ⧇ āφāĻĒāύāĻŋ āϚāĻžāϞāĻŋāϝāĻŧ⧇ āϝ⧇āϤ⧇ āϚāĻžāύ?", + "confirm_password": "āĻĒāĻžāϏāĻ“ā§ŸāĻžāĻ°ā§āĻĄ āĻĒ⧁āύāϰāĻžā§Ÿ āϞāĻŋāϖ⧁āύ", + "contain": "āĻŽāĻžāĻĒāĻŽāϤ", + "context": "āĻĒā§āϰāϏāĻ™ā§āĻ—", + "continue": "āĻāĻ—āĻŋā§Ÿā§‡ āϝāĻžāύ", + "copied_image_to_clipboard": "āĻ›āĻŦāĻŋ āĻ•ā§āϞāĻŋāĻĒāĻŦā§‹āĻ°ā§āĻĄā§‡ āĻ•āĻĒāĻŋ āĻšāϝāĻŧ⧇āϛ⧇āĨ¤", + "copied_to_clipboard": "āĻ•ā§āϞāĻŋāĻĒāĻŦā§‹āĻ°ā§āĻĄā§‡ āĻ•āĻĒāĻŋ āĻšāϝāĻŧ⧇āϛ⧇!", + "copy_error": "Error-āϟāĻŋ āĻ•āĻĒāĻŋ āĻ•āϰ⧁āύ", + "copy_file_path": "āĻĢāĻžāχāϞ āĻĒāĻžāĻĨ āĻ•āĻĒāĻŋ", + "copy_image": "āĻ›āĻŦāĻŋ āĻ•āĻĒāĻŋ", + "copy_link": "āϞāĻŋāĻ™ā§āĻ• āĻ•āĻĒāĻŋ", + "copy_link_to_clipboard": "āĻ•ā§āϞāĻŋāĻĒāĻŦā§‹āĻ°ā§āĻĄā§‡ āϞāĻŋāĻ™ā§āĻ• āĻ•āĻĒāĻŋ āĻ•āϰ⧁āύ", + "copy_password": "āĻĒāĻžāϏāĻ“āϝāĻŧāĻžāĻ°ā§āĻĄ āĻ•āĻĒāĻŋ āĻ•āϰ⧁āύ", + "copy_to_clipboard": "āĻ•ā§āϞāĻŋāĻĒāĻŦā§‹āĻ°ā§āĻĄā§‡ āĻ•āĻĒāĻŋ āĻ•āϰ⧁āύ", + "country": "āĻĻ⧇āĻļ", + "cover": "āϏāĻŽā§āĻĒā§‚āĻ°ā§āĻŖāĻ­āĻžāĻŦ⧇", + "covers": "āĻ•āĻ­āĻžāϰāϏ", + "create": "āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ", + "create_album": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽ āϤ⧈āϰāĻŋ", + "create_library": "āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āϤ⧈āϰāĻŋ", + "create_link": "āϞāĻŋāĻ™ā§āĻ• āϤ⧈āϰāĻŋ", + "create_link_to_share": "āĻļ⧇āϝāĻŧāĻžāϰ āϞāĻŋāĻ™ā§āĻ• āϤ⧈āϰāĻŋ", + "create_link_to_share_description": "āϞāĻŋāĻ™ā§āϕ⧇āϰ āĻŽāĻžāĻ§ā§āϝāĻŽā§‡ āϏāĻŦāĻžāχ āύāĻŋāĻ°ā§āĻŦāĻžāϚāĻŋāϤ āĻ›āĻŦāĻŋ āĻĻ⧇āĻ–āϤ⧇ āĻĒāĻžāϰāĻŦ⧇", + "create_new_person": "āύāϤ⧁āύ āĻŦā§āϝāĻ•ā§āϤāĻŋ āϝ⧋āĻ— āĻ•āϰ⧁āύ", + "create_new_person_hint": "āύāĻŋāĻ°ā§āĻŦāĻžāϚāĻŋāϤ āĻ…ā§āϝāĻžāϏ⧇āϟ āύāϤ⧁āύ āĻŦā§āϝāĻ•ā§āϤāĻŋāϰ āϏāĻ™ā§āϗ⧇ āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ", + "create_new_user": "āύāϤ⧁āύ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āϝ⧋āĻ— āĻ•āϰ⧁āύ", + "create_tag": "āĻŸā§āϝāĻžāĻ— āϤ⧈āϰāĻŋ", + "create_tag_description": "āύāϤ⧁āύ āĻŸā§āϝāĻžāĻ— āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύāĨ¤ āύ⧇āĻ¸ā§āĻŸā§‡āĻĄ āĻŸā§āϝāĻžāϗ⧇āϰ āĻ•ā§āώ⧇āĻ¤ā§āϰ⧇ āϏāĻŽā§āĻĒā§‚āĻ°ā§āĻŖ āĻĒāĻžāĻĨ - āĻĢāϰāĻ“āϝāĻŧāĻžāĻ°ā§āĻĄ āĻ¸ā§āĻ˛ā§āϝāĻžāĻļāϏāĻš āĻĻāĻŋāύāĨ¤", + "create_user": "āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āϝ⧋āĻ— āĻ•āϰ⧁āύ", + "created": "āϝ⧋āĻ— āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "current_device": "āϚāϞāϤāĻŋ āĻĄāĻŋāĻ­āĻžāχāϏ", + "custom_locale": "āĻ•āĻžāĻ¸ā§āϟāĻŽ āϞ⧋āϕ⧇āϞ", + "custom_locale_description": "āύāĻŋāĻ°ā§āĻŦāĻžāϚāĻŋāϤ āĻ­āĻžāώāĻž āĻāĻŦāĻ‚ āĻ…āĻžā§āϚāϞ⧇āϰ āĻ­āĻŋāĻ¤ā§āϤāĻŋāϤ⧇ āϤāĻžāϰāĻŋāĻ–, āϏāĻŽā§Ÿ āĻāĻŦāĻ‚ āϏāĻ‚āĻ–ā§āϝāĻž āĻĢāϰāĻŽā§āϝāĻžāϟ āĻ•āϰ⧁āύ", + "dark": "āĻĄāĻžāĻ°ā§āĻ•", + "date_after": "āĻāϰ āĻĒāϰ⧇āϰ āϤāĻžāϰāĻŋāĻ–", + "date_and_time": "āϤāĻžāϰāĻŋāĻ– āĻāĻŦāĻ‚ āϏāĻŽā§Ÿ", + "date_before": "āĻāϰ āφāϗ⧇āϰ āϤāĻžāϰāĻŋāĻ–", + "date_of_birth_saved": "āϜāĻ¨ā§āĻŽ āϤāĻžāϰāĻŋāĻ– āϏāĻĢāϞāĻ­āĻžāĻŦ⧇ āϏāĻ‚āϰāĻ•ā§āώāĻŖ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "delete": "āĻŽā§āϛ⧁āύ", + "delete_album": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽ āĻŽā§āϛ⧁āύ", + "delete_api_key_prompt": "āφāĻĒāύāĻŋ āĻ•āĻŋ āϏāĻ¤ā§āϝāĻŋāχ āĻāχ API key āĻŽā§āϛ⧇ āĻĢ⧇āϞāϤ⧇ āϚāĻžāύ?", + "delete_duplicates_confirmation": "āφāĻĒāύāĻŋ āĻ•āĻŋ āϏāĻ¤ā§āϝāĻŋāχ āĻāχ āĻĄā§āĻĒā§āϞāĻŋāϕ⧇āϟāϗ⧁āϞ⧋ āĻ¸ā§āĻĨāĻžā§Ÿā§€āĻ­āĻžāĻŦ⧇ āĻŽā§āĻ›āϤ⧇ āϚāĻžāύ?", + "delete_key": "key āĻŽā§āϛ⧁āύ", + "delete_library": "āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āĻŽā§āϛ⧁āύ", + "delete_link": "āϞāĻŋāĻ™ā§āĻ• āĻŽā§āϛ⧁āύ", + "delete_others": "āĻŦāĻžāĻ•āĻŋāϗ⧁āϞ⧋ āĻŽā§āϛ⧁āύ", + "delete_shared_link": "āĻļ⧇āϝāĻŧāĻžāϰ āĻ•āϰāĻž āϞāĻŋāĻ™ā§āĻ• āĻŽā§āϛ⧁āύ", + "delete_tag": "āĻŸā§āϝāĻžāĻ— āĻŽā§āϛ⧁āύ", + "delete_tag_confirmation_prompt": "āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤāĻ­āĻžāĻŦ⧇ {tagName} āĻŸā§āϝāĻžāĻ—āϟāĻŋ āĻŽā§āĻ›āϤ⧇ āϚāĻžāύ?", + "delete_user": "āχāωāϜāĻžāϰ āĻŽā§āϛ⧁āύ", + "deleted_shared_link": "āĻļ⧇āϝāĻŧāĻžāϰ āĻ•āϰāĻž āϞāĻŋāĻ™ā§āĻ•āϟāĻŋ āĻŽā§āϛ⧁āύ", + "deletes_missing_assets": "āĻĄāĻŋāĻ¸ā§āĻ• āĻĨ⧇āϕ⧇ āĻšāĻžāϰāĻžāύ⧋ āĻ…ā§āϝāĻžāϏ⧇āϟāϗ⧁āϞ⧋ āĻŽā§āϛ⧇", + "description": "āĻŦāĻŋāĻŦāϰāύ", + "details": "āĻŦāĻŋāĻ¸ā§āϤāĻžāϰāĻŋāϤ", + "direction": "āĻĻāĻŋāĻ•āύāĻŋāĻ°ā§āĻĻ⧇āĻļāύāĻž", + "disabled": "āύāĻŋāĻˇā§āĻ•ā§āϰāĻŋāϝāĻŧ", + "disallow_edits": "āϏāĻŽā§āĻĒāĻžāĻĻāύāĻž āĻ•āϰāĻžāϰ āĻ…āύ⧁āĻŽāϤāĻŋ āĻĻ⧇āĻŦ⧇āύ āύāĻž", + "discord": "āĻĄāĻŋāϏāĻ•āĻ°ā§āĻĄ", + "discover": "āĻĄāĻŋāϏāĻ•āĻ­āĻžāϰ", + "dismiss_all_errors": "āϏāĻŦ āĻ¤ā§āϰ⧁āϟāĻŋ āĻŦāĻžāϤāĻŋāϞ āĻ•āϰ⧁āύ", + "dismiss_error": "āĻ¤ā§āϰ⧁āϟāĻŋ āĻŦāĻžāϤāĻŋāϞ āĻ•āϰ⧁āύ", + "display_options": "āĻĄāĻŋāϏāĻĒā§āϞ⧇ āĻ…āĻĒāĻļāύ", + "display_order": "āĻĄāĻŋāϏāĻĒā§āϞ⧇ āĻ…āĻ°ā§āĻĄāĻžāϰ", + "display_original_photos": "āĻ…āϰāĻŋāϜāĻŋāύāĻžāϞ āĻ›āĻŦāĻŋ āĻĻ⧇āĻ–āĻžāύ", + "display_original_photos_setting_description": "āĻ…āϰāĻŋāϜāĻŋāύāĻžāϞ āĻ…ā§āϝāĻžāϏ⧇āϟāϟāĻŋ āĻ“āϝāĻŧ⧇āĻŦ-āϏāĻžāĻŽāĻžā§āϜāĻ¸ā§āϝāĻĒā§‚āĻ°ā§āĻŖ (web-compatible) āĻšāϞ⧇ āĻ…ā§āϝāĻžāϏ⧇āϟ āĻĻ⧇āĻ–āĻžāϰ āϏāĻŽāϝāĻŧ āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞ⧇āϰ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤ⧇ āĻŽā§‚āϞ āĻĢāĻŸā§‹āϟāĻŋ āĻĒā§āϰāĻĻāĻ°ā§āĻļāύ āĻ•āϰāϤ⧇ āĻ…āĻ—ā§āϰāĻžāϧāĻŋāĻ•āĻžāϰ āĻĻāĻŋāύāĨ¤ āĻāϰ āĻĢāϞ⧇ āĻĢāĻŸā§‹ āĻĒā§āϰāĻĻāĻ°ā§āĻļāύ⧇āϰ āĻ—āϤāĻŋ āĻ•āĻŋāϛ⧁āϟāĻž āϧ⧀āϰ āĻšāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", + "do_not_show_again": "āĻāχ āĻŽā§‡āϏ⧇āϜāϟāĻŋ āφāϰ āĻĻ⧇āĻ–āĻžāĻŦ⧇āύ āύāĻž", + "documentation": "āϏāĻšāĻžā§ŸāĻ• āύāĻŋāĻ°ā§āĻĻ⧇āĻļāĻŋāĻ•āĻž", + "done": "āϏāĻŽā§āĻĒāĻ¨ā§āύ", + "download": "āĻĄāĻžāωāύāϞ⧋āĻĄ", + "download_include_embedded_motion_videos": "āĻāĻŽāĻŦ⧇āĻĄā§‡āĻĄ āĻ­āĻŋāĻĄāĻŋāĻ“", + "download_include_embedded_motion_videos_description": "āĻŽā§‹āĻļāύ āĻĢāĻŸā§‹āϰ (motion photos) āĻŽāĻ§ā§āϝ⧇ āĻĨāĻžāĻ•āĻž āĻ­āĻŋāĻĄāĻŋāĻ“āϗ⧁āϞ⧋āϕ⧇ āφāϞāĻžāĻĻāĻž āĻĢāĻžāχāϞ āĻšāĻŋāϏ⧇āĻŦ⧇ āĻ…āĻ¨ā§āϤāĻ°ā§āϭ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ", + "download_settings": "āĻĄāĻžāωāύāϞ⧋āĻĄ", + "download_settings_description": "āĻ…ā§āϝāĻžāϏ⧇āϟ āĻĄāĻžāωāύāϞ⧋āĻĄā§‡āϰ āϏ⧇āϟāĻŋāĻ‚āϏ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ", + "open_in_browser": "āĻŦā§āϰāĻžāωāϜāĻžāϰ⧇ āĻ“āĻĒ⧇āύ āĻ•āϰ⧁āύ", "user_usage_stats": "āĻ…ā§āϝāĻžāĻ•āĻžāωāĻ¨ā§āϟ āĻŦā§āϝāĻŦāĻšāĻžāϰ⧇āϰ āĻĒāϰāĻŋāϏāĻ‚āĻ–ā§āϝāĻžāύ", "user_usage_stats_description": "āĻ…ā§āϝāĻžāĻ•āĻžāωāĻ¨ā§āϟ āĻŦā§āϝāĻŦāĻšāĻžāϰ⧇āϰ āĻĒāϰāĻŋāϏāĻ‚āĻ–ā§āϝāĻžāύ āĻĻ⧇āϖ⧁āύ", "yes": "āĻšā§āϝāĻžāρ", diff --git a/i18n/ca.json b/i18n/ca.json index fbd6c5840f..88c6b8a323 100644 --- a/i18n/ca.json +++ b/i18n/ca.json @@ -372,7 +372,7 @@ "transcoding_audio_codec": "CÃ˛dec d'àudio", "transcoding_audio_codec_description": "Opus Ês l'opciÃŗ de màxima qualitat, perÃ˛ tÊ menor compatibilitat amb dispositius o programari antics.", "transcoding_bitrate_description": "Vídeos superiors a la taxa de bits màxima o que no tenen un format acceptat", - "transcoding_codecs_learn_more": "Per obtenir mÊs informaciÃŗ sobre la terminologia utilitzada, consulteu la documentaciÃŗ de FFmpeg per al cÃ˛dec H.264, cÃ˛dec HEVC i cÃ˛dec VP9.", + "transcoding_codecs_learn_more": "Per obtenir mÊs informaciÃŗ sobre la terminologia utilitzada, consulteu la documentaciÃŗ de FFmpeg per al cÃ˛dec H.264, cÃ˛dec HEVC i cÃ˛dec VP9.", "transcoding_constant_quality_mode": "Mode de qualitat constant", "transcoding_constant_quality_mode_description": "ICQ Ês millor que CQP, perÃ˛ alguns dispositius d'acceleraciÃŗ de maquinari no admeten aquest mode. Establir aquesta opciÃŗ preferirà el mode especificat quan utilitzeu la codificaciÃŗ basada en la qualitat. Ignorat per NVENC perquè no Ês compatible amb ICQ.", "transcoding_constant_rate_factor": "Factor de taxa constant (-crf)", @@ -441,7 +441,7 @@ "user_successfully_removed": "L'usuari {email} s'ha eliminat correctament.", "users_page_description": "Pàgina d'usuaris de l'administrador", "version_check_enabled_description": "Activa la comprovaciÃŗ de la versiÃŗ", - "version_check_implications": "La funciÃŗ de comprovaciÃŗ de versions depèn de comunicacions periÃ˛diques amb github.com", + "version_check_implications": "La funciÃŗ de comprovaciÃŗ de versions depèn de comunicacions periÃ˛diques amb {server}", "version_check_settings": "ComprovaciÃŗ de versiÃŗ", "version_check_settings_description": "Activa/desactiva la notificaciÃŗ de nova versiÃŗ", "video_conversion_job": "TranscodificaciÃŗ de vídeos", @@ -849,9 +849,12 @@ "create_link_to_share": "Crear enllaç per compartir", "create_link_to_share_description": "Deixa que qualsevol persona amb l'enllaç vegi les fotos seleccionades", "create_new": "CREAR NOU", + "create_new_face": "Crea una nova cara", "create_new_person": "Crea una nova persona", "create_new_person_hint": "Assigna els elements seleccionats a una persona nova", "create_new_user": "Crea un usuari nou", + "create_person": "Crea una persona", + "create_person_subtitle": "Afegeix un nom a la cara seleccionada per crear i etiquetar la nova persona", "create_shared_album_page_share_add_assets": "AFEGEIX ELEMENTS", "create_shared_album_page_share_select_photos": "Escull fotografies", "create_shared_link": "Crea un enllaç compartit", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "Fixat", "crop_aspect_ratio_free": "Lliure", "crop_aspect_ratio_original": "Original", + "crop_aspect_ratio_square": "Quadrat", "curated_object_page_title": "Coses", "current_device": "Dispositiu actual", "current_pin_code": "Codi PIN actual", @@ -880,7 +884,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Fosc", - "dark_theme": "Canviar a tema fosc", + "dark_theme": "Canvia a tema fosc", "date": "Data", "date_after": "Data posterior a", "date_and_time": "Data i hora", @@ -891,10 +895,8 @@ "day": "Dia", "days": "Dies", "deduplicate_all": "Desduplica-ho tot", - "deduplication_criteria_1": "Mida d'imatge en bytes", - "deduplication_criteria_2": "Quantitat de dades EXIF", - "deduplication_info": "InformaciÃŗ de deduplicaciÃŗ", - "deduplication_info_description": "Per preseleccionar recursos automàticament i eliminar els duplicats de manera massiva, ens fixem en:", + "default_locale": "ConfiguraciÃŗ regional predeterminada", + "default_locale_description": "Format de dades i nÃēmeros en funciÃŗ de la configuraciÃŗ local", "delete": "Esborrar", "delete_action_confirmation_message": "Segur que vols eliminar aquest recurs? Aquesta acciÃŗ el mourà a la paperera del servidor, i et preguntarà si el vols eliminar localment", "delete_action_prompt": "{count} eliminats", @@ -970,7 +972,7 @@ "downloading_media": "Descàrrega multimèdia", "drop_files_to_upload": "Deixeu els fitxers a qualsevol lloc per pujar-los", "duplicates": "Duplicats", - "duplicates_description": "Resol cada grup indicant, si n'hi ha, quins sÃŗn duplicats", + "duplicates_description": "Resol cada grup indicant, si n'hi ha, quins sÃŗn duplicats.", "duration": "Durada", "edit": "Editar", "edit_album": "Edita l'àlbum", @@ -992,7 +994,7 @@ "edit_location_dialog_title": "UbicaciÃŗ", "edit_name": "Edita el nom", "edit_people": "Edita la gent", - "edit_tag": "Editar etiqueta", + "edit_tag": "Edita etiqueta", "edit_title": "Edita títol", "edit_user": "Edita l'usuari", "edit_workflow": "Edita el flux de treball", @@ -1007,6 +1009,8 @@ "editor_edits_applied_success": "Les modificacions s'han aplicat correctament", "editor_flip_horizontal": "Capgira horitzontalment", "editor_flip_vertical": "Capgira verticalment", + "editor_handle_corner": "{corner, select, top_left {Top-left} top_right {Top-right} bottom_left {Bottom-left} bottom_right {Bottom-right} other {A}} cantÃŗ per agafar", + "editor_handle_edge": "{edge, select, top {Top} bottom {Bottom} left {Left} right {Right} other {An}} cantÃŗ per agafar", "editor_orientation": "OrientaciÃŗ", "editor_reset_all_changes": "Reiniciar canvis", "editor_rotate_left": "Rota 90Âē al contrari de les agulles", @@ -1168,7 +1172,7 @@ "exif_bottom_sheet_description_error": "No s'ha pogut actualitzar la descripciÃŗ", "exif_bottom_sheet_details": "DETALLS", "exif_bottom_sheet_location": "UBICACIÓ", - "exif_bottom_sheet_no_description": "Sense descriociÃŗ", + "exif_bottom_sheet_no_description": "Sense descripciÃŗ", "exif_bottom_sheet_people": "PERSONES", "exif_bottom_sheet_person_add_person": "Afegir nom", "exit_slideshow": "Surt de la presentaciÃŗ de diapositives", @@ -1385,9 +1389,11 @@ "library_page_sort_title": "Títol de l'àlbum", "licenses": "Llicències", "light": "Llum", + "light_theme": "Canviar a tema clar", "like": "M'agrada", "like_deleted": "M'agrada suprimit", "link_motion_video": "Enllaçar vídeo en moviment", + "link_to_docs": "Per mÊs informaciÃŗ, mirar la documentation.", "link_to_oauth": "Enllaç a OAuth", "linked_oauth_account": "Compte OAuth enllaçat", "list": "Llista", @@ -1649,6 +1655,7 @@ "only_favorites": "NomÊs preferits", "open": "Obrir", "open_calendar": "Obrir el calendari", + "open_in_browser": "Obre al navegador", "open_in_map_view": "Obrir a la vista del mapa", "open_in_openstreetmap": "Obre a OpenStreetMap", "open_the_search_filters": "Obriu els filtres de cerca", @@ -2210,6 +2217,7 @@ "tag": "Etiqueta", "tag_assets": "Etiquetar actius", "tag_created": "Etiqueta creada: {tag}", + "tag_face": "Etiqueta una cara", "tag_feature_description": "Exploreu fotos i vídeos agrupats per temes d'etiquetes lÃ˛giques", "tag_not_found_question": "No trobeu una etiqueta? Crear una nova etiqueta.", "tag_people": "Etiquetar personas", @@ -2391,6 +2399,7 @@ "viewer_remove_from_stack": "Elimina de la pila", "viewer_stack_use_as_main_asset": "Fes servir com a element principal", "viewer_unstack": "Desapila", + "visibility": "Visibilitat", "visibility_changed": "La visibilitat ha canviat per {count, plural, one {# persona} other {# persones}}", "visual": "Visual", "visual_builder": "Constructor visual", diff --git a/i18n/cs.json b/i18n/cs.json index 363e568331..0c333532f3 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -441,7 +441,7 @@ "user_successfully_removed": "UÅživatel {email} byl ÃēspÄ›ÅĄně odstraněn.", "users_page_description": "StrÃĄnka sprÃĄvců", "version_check_enabled_description": "Povolit kontrolu verzí", - "version_check_implications": "Kontrola verze je zaloÅžena na pravidelnÊ komunikaci s github.com", + "version_check_implications": "Kontrola verze je zaloÅžena na pravidelnÊ komunikaci s {server}", "version_check_settings": "Kontrola verze", "version_check_settings_description": "Povolení/zakÃĄzÃĄní oznÃĄmení o novÊ verzi", "video_conversion_job": "PřekÃŗdovÃĄní videí", @@ -849,9 +849,12 @@ "create_link_to_share": "Vytvořit odkaz pro sdílení", "create_link_to_share_description": "UmoÅžnit kaÅždÊmu, kdo mÃĄ odkaz, zobrazit vybranÊ fotografie", "create_new": "VYTVOŘIT NOVÉ", + "create_new_face": "Vytvořit novÃŊ obličej", "create_new_person": "Vytvořit novou osobu", "create_new_person_hint": "Přiřadit vybranÊ poloÅžky novÊ osobě", "create_new_user": "Vytvořit novÊho uÅživatele", + "create_person": "Vytvořit osobu", + "create_person_subtitle": "Přidejte jmÊno ke zvolenÊmu obličeji pro vytvoření a označení novÊ osoby", "create_shared_album_page_share_add_assets": "PŘIDAT POLOÅŊKY", "create_shared_album_page_share_select_photos": "Vybrat fotografie", "create_shared_link": "Vytvořit sdílenÃŊ odkaz", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "PevnÃŊ", "crop_aspect_ratio_free": "VolnÃŊ", "crop_aspect_ratio_original": "Původní", + "crop_aspect_ratio_square": "Čtverec", "curated_object_page_title": "Věci", "current_device": "SoučasnÊ zařízení", "current_pin_code": "AktuÃĄlní PIN kÃŗd", @@ -880,7 +884,7 @@ "daily_title_text_date": "EEEE, d. MMMM", "daily_title_text_date_year": "EEEE, d. MMMM y", "dark": "TmavÃŊ", - "dark_theme": "Přepnout tmavÃŊ motiv", + "dark_theme": "Přepnout na tmavÃŊ motiv", "date": "Datum", "date_after": "Datum po", "date_and_time": "Datum a čas", @@ -891,10 +895,8 @@ "day": "Den", "days": "Dnů", "deduplicate_all": "Odstranit vÅĄechny duplicity", - "deduplication_criteria_1": "Velikost obrÃĄzku v bajtech", - "deduplication_criteria_2": "Počet EXIF dat", - "deduplication_info": "Informace o deduplikaci", - "deduplication_info_description": "Pro automatickÃŊ předvÃŊběr poloÅžek a hromadnÊ odstranění duplicit se zohledňuje:", + "default_locale": "VÃŊchozí nÃĄrodní prostředí", + "default_locale_description": "FormÃĄtovÃĄní datumu a čísel podle místního nastavení prohlíŞeče", "delete": "Smazat", "delete_action_confirmation_message": "Opravdu chcete odstranit tuto poloÅžku? Tato akce přesune poloÅžku do serverovÊho koÅĄe a zeptÃĄ se vÃĄs, zda ji chcete odstranit lokÃĄlně", "delete_action_prompt": "{count} smazÃĄno", @@ -970,7 +972,7 @@ "downloading_media": "StahovÃĄní mÊdia", "drop_files_to_upload": "Pro nahrÃĄní sem přetÃĄhněte soubory", "duplicates": "Duplicity", - "duplicates_description": "VyřeÅĄte kaÅždou skupinu tak, Åže uvedete, kterÊ skupiny jsou duplicitní", + "duplicates_description": "VyřeÅĄte kaÅždou skupinu tak, Åže uvedete, kterÊ skupiny jsou duplicitní.", "duration": "Doba trvÃĄní", "edit": "Upravit", "edit_album": "Upravit album", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "Podle nÃĄzvu alba", "licenses": "Licence", "light": "SvětlÃŊ", + "light_theme": "Přepnout na světlÃŊ motiv", "like": "Líbí se mi", "like_deleted": "Oblíbení smazÃĄno", "link_motion_video": "Připojit pohyblivÊ video", + "link_to_docs": "DalÅĄÃ­ informace najdete v dokumentaci.", "link_to_oauth": "Propojit s OAuth", "linked_oauth_account": "PropojenÃŊ OAuth Ãēčet", "list": "Seznam", @@ -2213,6 +2217,7 @@ "tag": "Značka", "tag_assets": "Přiřadit značku", "tag_created": "Vytvořena značka: {tag}", + "tag_face": "Označit obličej", "tag_feature_description": "ProchÃĄzení fotografií a videí seskupenÃŊch podle tÊmat logickÃŊch značek", "tag_not_found_question": "NemůŞete najít značku? Vytvořte novou.", "tag_people": "Označit lidi", @@ -2394,6 +2399,7 @@ "viewer_remove_from_stack": "Odstranit ze seskupení", "viewer_stack_use_as_main_asset": "PouŞít jako hlavní poloÅžku", "viewer_unstack": "ZruÅĄit seskupení", + "visibility": "Viditelnost", "visibility_changed": "Viditelnost změněna u {count, plural, one {# osoby} few {# osob} other {# lidí}}", "visual": "VizuÃĄlní", "visual_builder": "VizuÃĄlní nÃĄvrhÃĄÅ™", diff --git a/i18n/da.json b/i18n/da.json index 1eafb3d827..7628be0f4c 100644 --- a/i18n/da.json +++ b/i18n/da.json @@ -441,7 +441,7 @@ "user_successfully_removed": "Bruger {email} er blevet fjernet med succes.", "users_page_description": "Admin-brugere side", "version_check_enabled_description": "AktivÊr versionstjek", - "version_check_implications": "Funktionen til versionstjek er afhÃĻngig af periodisk kommunikation med github.com", + "version_check_implications": "Funktionen til versionstjek er afhÃĻngig af periodisk kommunikation med {server}", "version_check_settings": "Versionstjek", "version_check_settings_description": "Aktiver/deaktiverer notifikation for den nye version", "video_conversion_job": "Transkod videoer", @@ -849,9 +849,12 @@ "create_link_to_share": "Opret link for at dele", "create_link_to_share_description": "Tillad alle med linket at se de(t) valgte billede(r)", "create_new": "OPRET NY", + "create_new_face": "Opret nyt ansigt", "create_new_person": "Opret ny person", "create_new_person_hint": "Tildel valgte aktiver til en ny person", "create_new_user": "Opret ny bruger", + "create_person": "Opret person", + "create_person_subtitle": "Tilføj et navn til det valgte ansigt for at oprette og tagge den nye person", "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", @@ -863,9 +866,10 @@ "created_at": "Oprettet", "creating_linked_albums": "Opretter sammenkÃĻdede albums...", "crop": "BeskÃĻr", - "crop_aspect_ratio_fixed": "Fikset", - "crop_aspect_ratio_free": "Gratis", + "crop_aspect_ratio_fixed": "Fast", + "crop_aspect_ratio_free": "Fri", "crop_aspect_ratio_original": "Original", + "crop_aspect_ratio_square": "Kvadrat", "curated_object_page_title": "Ting", "current_device": "NuvÃĻrende enhed", "current_pin_code": "NuvÃĻrende PIN kode", @@ -890,11 +894,9 @@ "date_range": "Datointerval", "day": "Dag", "days": "Dage", - "deduplicate_all": "Kopier alle", - "deduplication_criteria_1": "Billedstørrelse i bytes", - "deduplication_criteria_2": "Antal EXIF-data", - "deduplication_info": "Deduplikerings info", - "deduplication_info_description": "For automatisk at forudvÃĻlge emner og fjerne dubletter i bulk ser vi pÃĨ:", + "deduplicate_all": "Dedubliker alle", + "default_locale": "Standard sprog", + "default_locale_description": "FormatÊr datoer og tal baseret pÃĨ din browsers landestandard", "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", @@ -970,7 +972,7 @@ "downloading_media": "Download medier", "drop_files_to_upload": "Slip filer hvor som helst for at uploade dem", "duplicates": "Duplikater", - "duplicates_description": "Løs hver gruppe ved at angive, hvilke, hvis nogen, er dubletter", + "duplicates_description": "Løs hver gruppe ved at angive hvilke, hvis nogen, er dubletter", "duration": "Varighed", "edit": "Rediger", "edit_album": "RedigÊr album", @@ -1007,6 +1009,8 @@ "editor_edits_applied_success": "Redigeringer gemt", "editor_flip_horizontal": "Vend horisontalt", "editor_flip_vertical": "Flip vertikal", + "editor_handle_corner": "{corner, select, top_left {Øverst venstre} top_right {Øverst højre} bottom_left {Nederst venstre} bottom_right {Nederst højre} other {A}} hjørnehÃĨndtag", + "editor_handle_edge": "{edge, select, top {Øverst} bottom {Nederst} left {Venstre} right {Højre} other {Et}} kanthÃĨndtag", "editor_orientation": "Orientering", "editor_reset_all_changes": "Nulstil ÃĻndringer", "editor_rotate_left": "RotÊr 90° mod uret", @@ -1017,7 +1021,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_backup": "AktivÊr backup", "enable_biometric_auth_description": "Indtast din PIN kode for at slÃĨ biometrisk adgangskontrol til", "enabled": "Aktiveret", "end_date": "Slutdato", @@ -1072,7 +1076,7 @@ "failed_to_update_notification_status": "Kunne ikke uploade notifikations status", "incorrect_email_or_password": "Forkert email eller kodeord", "library_folder_already_exists": "Denne import sti findes allerede.", - "page_not_found": "Siden blev ikke fundet :/", + "page_not_found": "Siden blev ikke fundet", "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", @@ -1385,9 +1389,11 @@ "library_page_sort_title": "Albumtitel", "licenses": "Licenser", "light": "Lys", + "light_theme": "Skift til lyst tema", "like": "Synes om", "like_deleted": "Ligesom slettet", "link_motion_video": "Link bevÃĻgelsesvideo", + "link_to_docs": "For yderligere information, se dokumentationen.", "link_to_oauth": "Link til OAuth", "linked_oauth_account": "Tilsluttet OAuth-konto", "list": "Liste", @@ -1649,6 +1655,7 @@ "only_favorites": "Kun favoritter", "open": "Åben", "open_calendar": "Åbn kalender", + "open_in_browser": "Åbn i browser", "open_in_map_view": "Åben i kortvisning", "open_in_openstreetmap": "Åben i OpenStreetMap", "open_the_search_filters": "Åbn søgefiltre", @@ -2210,6 +2217,7 @@ "tag": "Tag", "tag_assets": "Tag mediefiler", "tag_created": "Oprettet tag: {tag}", + "tag_face": "Tag ansigt", "tag_feature_description": "Gennemse billeder og videoer grupperet efter logiske tag-emner", "tag_not_found_question": "Kan du ikke finde et tag? Opret et nyt tag.", "tag_people": "Tag personer", @@ -2391,6 +2399,7 @@ "viewer_remove_from_stack": "Fjern fra stak", "viewer_stack_use_as_main_asset": "Brug som hovedelement", "viewer_unstack": "Fjern fra stak", + "visibility": "Synlighed", "visibility_changed": "Synlighed ÃĻndret for {count, plural, one {# person} other {# personer}}", "visual": "Visuel", "visual_builder": "Visuel builder", diff --git a/i18n/de.json b/i18n/de.json index a6b2a844c4..9816055023 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -1,5 +1,5 @@ { - "about": "Über Immich", + "about": "Über", "account": "Konto", "account_settings": "Kontoeinstellungen", "acknowledge": "Verstanden", @@ -8,7 +8,7 @@ "action_description": "Eine Reihe von Aktionen, die an den gefilterten Assets ausgefÃŧhrt werden sollen", "actions": "Aktionen", "active": "Aktiv", - "active_count": "Aktive:{count}", + "active_count": "Aktive: {count}", "activity": "Aktivität", "activity_changed": "Aktivität ist {enabled, select, true {aktiviert} other {deaktiviert}}", "add": "HinzufÃŧgen", @@ -59,7 +59,7 @@ "backup_database_enable_description": "Datenbank regelmäßig sichern", "backup_keep_last_amount": "Anzahl der aufzubewahrenden frÃŧheren Sicherungen", "backup_onboarding_1_description": "Offsite-Kopie in der Cloud oder an einem anderen physischen Ort.", - "backup_onboarding_2_description": "lokale Kopien auf verschiedenen Geräten. Dazu gehÃļren die Hauptdateien und eine lokale Sicherung dieser Dateien.", + "backup_onboarding_2_description": "Lokale Kopien auf verschiedenen Geräten. Dazu gehÃļren die Hauptdateien und eine lokale Sicherung dieser Dateien.", "backup_onboarding_3_description": "Kopien deiner Daten inklusive Originaldateien. Dies umfasst 1 Kopie an einem anderen Ort und 2 lokale Kopien.", "backup_onboarding_description": "Eine 3-2-1 Sicherungsstrategie wird empfohlen, um deine Daten zu schÃŧtzen. Du solltest sowohl Kopien deiner hochgeladenen Fotos/Videos als auch der Immich-Datenbank aufbewahren, um eine umfassende SicherungslÃļsung zu haben.", "backup_onboarding_footer": "Weitere Informationen zum Sichern von Immich findest du in der Dokumentation.", @@ -309,7 +309,7 @@ "reset_settings_to_recent_saved": "Einstellungen auf die zuletzt gespeicherten Einstellungen zurÃŧcksetzen", "scanning_library": "Bibliothek scannen", "search_jobs": "Suchaufgabenâ€Ļ", - "send_welcome_email": "BegrÃŧssungsmail senden", + "send_welcome_email": "BegrÃŧßungsmail senden", "server_external_domain_settings": "Externe Domain", "server_external_domain_settings_description": "FÃŧr externe Links verwendete Domäne", "server_public_users": "Öffentliche Benutzer", @@ -441,7 +441,7 @@ "user_successfully_removed": "Der Benutzer {email} wurde erfolgreich entfernt.", "users_page_description": "Administrator-Benutzerseite", "version_check_enabled_description": "VersionsprÃŧfung aktivieren", - "version_check_implications": "Die Funktion zur VersionsprÃŧfung basiert auf regelmäßiger Kommunikation mit GitHub.com", + "version_check_implications": "Die Funktion zur VersionsprÃŧfung basiert auf regelmäßiger Kommunikation mit {server}", "version_check_settings": "VersionsprÃŧfung", "version_check_settings_description": "Aktivieren/Deaktivieren der Benachrichtigung Ãŧber neue Versionen", "video_conversion_job": "Videos transkodieren", @@ -472,7 +472,7 @@ "advanced_settings_troubleshooting_title": "Fehlersuche", "age_months": "Alter {months, plural, one {# Monat} other {# Monate}}", "age_year_months": "Alter 1 Jahr, {months, plural, one {# Monat} other {# Monate}}", - "age_years": "Alter {years, plural, one {# Jahr} other {# Jahre}}", + "age_years": "{years, plural, other {Alter #}}", "album": "Album", "album_added": "Album hinzugefÃŧgt", "album_added_notification_setting_description": "Erhalte eine E-Mail-Benachrichtigung, wenn du zu einem freigegebenen Album hinzugefÃŧgt wurdest", @@ -541,7 +541,7 @@ "app_settings": "App-Einstellungen", "app_stores": "App Stores", "app_update_available": "App Update verfÃŧgbar", - "appears_in": "Erscheint in", + "appears_in": "Enthalten in", "apply_count": "Anwenden ({count, number})", "archive": "Archiv", "archive_action_prompt": "{count} zum Archiv hinzugefÃŧgt", @@ -580,7 +580,7 @@ "asset_restored_successfully": "Datei erfolgreich wiederhergestellt", "asset_skipped": "Übersprungen", "asset_skipped_in_trash": "Im Papierkorb", - "asset_trashed": "Datei GelÃļscht", + "asset_trashed": "Datei gelÃļscht", "asset_troubleshoot": "Datei Fehlerbehebung", "asset_uploaded": "Hochgeladen", "asset_uploading": "Hochladenâ€Ļ", @@ -610,14 +610,14 @@ "assets_were_part_of_album_count": "{count, plural, one {# Datei ist} other {# Dateien sind}} bereits im Album vorhanden", "assets_were_part_of_albums_count": "{count, plural, one {Datei war} other {Dateien waren}} bereits in den Alben", "authorized_devices": "Verwendete Geräte", - "automatic_endpoint_switching_subtitle": "Verbinden Sie sich lokal Ãŧber ein bestimmtes WiFi, wenn es verfÃŧgbar ist, und verwenden Sie andere VerbindungsmÃļglichkeiten", + "automatic_endpoint_switching_subtitle": "Verbinden Sie sich lokal Ãŧber ein bestimmtes WLAN-Netz, wenn es verfÃŧgbar ist, und verwenden Sie ansonsten andere VerbindungsmÃļglichkeiten", "automatic_endpoint_switching_title": "Automatische URL-Umschaltung", "autoplay_slideshow": "Automatische Diashow", "back": "ZurÃŧck", "back_close_deselect": "ZurÃŧck, Schließen oder Abwählen", "background_backup_running_error": "Sicherung läuft im Hintergrund. Manuelle Sicherung kann nicht gestartet werden", "background_location_permission": "Hintergrund Standortfreigabe", - "background_location_permission_content": "Um im Hintergrund zwischen den Netzwerken wechseln zu kÃļnnen, muss Immich *immer* Zugriff auf den genauen Standort haben, damit die App den Namen des WiFi-Netzwerks ermitteln kann", + "background_location_permission_content": "Um im Hintergrund zwischen den Netzwerken wechseln zu kÃļnnen, muss Immich *immer* Zugriff auf den genauen Standort haben, damit die App den Namen des WLAN-Netzwerks ermitteln kann", "background_options": "Hintergrund Optionen", "backup": "Sicherung", "backup_album_selection_page_albums_device": "Alben auf dem Gerät ({count})", @@ -652,7 +652,7 @@ "backup_controller_page_background_is_on": "Automatische Sicherung im Hintergrund ist aktiviert", "backup_controller_page_background_turn_off": "Hintergrundservice ausschalten", "backup_controller_page_background_turn_on": "Hintergrundservice einschalten", - "backup_controller_page_background_wifi": "Nur im WiFi", + "backup_controller_page_background_wifi": "Nur im WLAN", "backup_controller_page_backup": "Sicherung", "backup_controller_page_backup_selected": "Ausgewählt: ", "backup_controller_page_backup_sub": "Gesicherte Fotos und Videos", @@ -687,7 +687,7 @@ "backup_options_page_title": "Sicherungsoptionen", "backup_setting_subtitle": "Verwaltung der Upload-Einstellungen im Hintergrund und im Vordergrund", "backup_settings_subtitle": "Upload-Einstellungen verwalten", - "backup_upload_details_page_more_details": "Tippen fÃŧr weitere Details", + "backup_upload_details_page_more_details": "Tippe fÃŧr weitere Details", "backward": "RÃŧckwärts", "biometric_auth_enabled": "Biometrische Authentifizierung aktiviert", "biometric_locked_out": "Du bist von der biometrischen Authentifizierung ausgeschlossen", @@ -697,8 +697,8 @@ "birthdate_set_description": "Das Geburtsdatum wird verwendet, um das Alter dieser Person zum Zeitpunkt eines Fotos zu berechnen.", "blurred_background": "Unscharfer Hintergrund", "bugs_and_feature_requests": "Fehler & Verbesserungsvorschläge", - "build": "Erstelle", - "build_image": "Bild erstellen", + "build": "Build", + "build_image": "Abbildversion", "bulk_delete_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien gemeinsam}} lÃļschen mÃļchtest? Dabei wird die grÃļßte Datei jeder Gruppe behalten und alle anderen Duplikate endgÃŧltig gelÃļscht. Diese Aktion kann nicht rÃŧckgängig gemacht werden!", "bulk_keep_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} behalten mÃļchtest? Dies wird alle Duplikat-Gruppen auflÃļsen ohne etwas zu lÃļschen.", "bulk_trash_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien gemeinsam}} in den Papierkorb verschieben mÃļchtest? Dies wird die grÃļßte Datei jeder Gruppe behalten und alle anderen Duplikate in den Papierkorb verschieben.", @@ -728,7 +728,7 @@ "cannot_undo_this_action": "Diese Aktion kann nicht rÃŧckgängig gemacht werden!", "cannot_update_the_description": "Beschreibung kann nicht aktualisiert werden", "cast": "Übertragen", - "cast_description": "Konfiguration verfÃŧgbarer Ziele", + "cast_description": "VerfÃŧgbare Cast-Ziele konfigurieren", "change_date": "Datum ändern", "change_description": "Beschreibung anpassen", "change_display_order": "Anzeigereihenfolge ändern", @@ -739,7 +739,7 @@ "change_password": "Passwort ändern", "change_password_description": "Dies ist entweder das erste Mal, dass du dich im System anmeldest, oder es wurde eine Anfrage zur Änderung deines Passworts gestellt. Bitte gib unten dein neues Passwort ein.", "change_password_form_confirm_password": "Passwort bestätigen", - "change_password_form_description": "Hallo {name}\n\nDas ist entweder das erste Mal dass du dich einloggst oder es wurde eine Anfrage zur Änderung deines Passwortes gestellt. Bitte gib das neue Passwort ein.", + "change_password_form_description": "Hallo {name}\n\nDas ist entweder das erste Mal, dass du dich einloggst oder es wurde eine Anfrage zur Änderung deines Passwortes gestellt. Bitte gib das neue Passwort ein.", "change_password_form_log_out": "Von allen Geräte abmelden", "change_password_form_log_out_description": "Es wird empfohlen, alle anderen Geräte abzumelden", "change_password_form_new_password": "Neues Passwort", @@ -754,7 +754,7 @@ "charging_requirement_mobile_backup": "Backup im Hintergrund erfordert Aufladen des Geräts", "check_corrupt_asset_backup": "Auf beschädigte Asset-Backups ÃŧberprÃŧfen", "check_corrupt_asset_backup_button": "ÜberprÃŧfung durchfÃŧhren", - "check_corrupt_asset_backup_description": "FÃŧhre diese PrÃŧfung nur mit aktivierten WiFi durch, nachdem alle Dateien gesichert worden sind. Dieser Vorgang kann ein paar Minuten dauern.", + "check_corrupt_asset_backup_description": "FÃŧhre diese PrÃŧfung nur mit aktivierten WLAN durch, nachdem alle Dateien gesichert worden sind. Dieser Vorgang kann ein paar Minuten dauern.", "check_logs": "Logs prÃŧfen", "checksum": "PrÃŧfsumme", "choose_matching_people_to_merge": "Wähle passende Personen zum ZusammenfÃŧhren", @@ -807,13 +807,13 @@ "completed": "Abgeschlossen", "confirm": "Bestätigen", "confirm_admin_password": "Administrator Passwort bestätigen", - "confirm_delete_face": "Bist du sicher dass du das Gesicht von {name} aus der Datei entfernen willst?", + "confirm_delete_face": "Bist du sicher, dass du das Gesicht von {name} aus der Datei entfernen willst?", "confirm_delete_shared_link": "Bist du sicher, dass du diesen geteilten Link lÃļschen willst?", "confirm_keep_this_delete_others": "Alle anderen Dateien im Stapel bis auf diese werden gelÃļscht. Bist du sicher, dass du fortfahren mÃļchten?", "confirm_new_pin_code": "Neuen PIN-Code bestätigen", "confirm_password": "Passwort bestätigen", - "confirm_tag_face": "Wollen Sie dieses Gesicht mit {name} markieren?", - "confirm_tag_face_unnamed": "MÃļchten Sie dieses Gesicht markieren?", + "confirm_tag_face": "Wollen Sie dieses Gesicht mit {name} taggen?", + "confirm_tag_face_unnamed": "MÃļchten Sie dieses Gesicht taggen?", "connected_device": "Verbundenes Gerät", "connected_to": "Verbunden mit", "contain": "Vollständig", @@ -849,9 +849,12 @@ "create_link_to_share": "Link zum Teilen erstellen", "create_link_to_share_description": "Lass jeden mit dem Link die ausgewählten Fotos sehen", "create_new": "NEUES ERSTELLEN", + "create_new_face": "Neues Gesicht erstellen", "create_new_person": "Neue Person anlegen", "create_new_person_hint": "Ausgewählte Dateien einer neuen Person zuweisen", "create_new_user": "Neuen Nutzer erstellen", + "create_person": "Person anlegen", + "create_person_subtitle": "Gib dem gewählten Gesicht einen Namen um die neue Person zu erstellen und zu taggen", "create_shared_album_page_share_add_assets": "INHALTE HINZUFÜGEN", "create_shared_album_page_share_select_photos": "Fotos auswählen", "create_shared_link": "Geteilten Link erstellen", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "Fixiert", "crop_aspect_ratio_free": "Frei", "crop_aspect_ratio_original": "Original", + "crop_aspect_ratio_square": "Quadratisch", "curated_object_page_title": "Dinge", "current_device": "Aktuelles Gerät", "current_pin_code": "Aktueller PIN-Code", @@ -880,7 +884,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Dunkel", - "dark_theme": "Dunkle Ansicht umschalten", + "dark_theme": "Auf dunkle Ansicht umschalten", "date": "Datum", "date_after": "Datum nach", "date_and_time": "Datum und Zeit", @@ -891,10 +895,8 @@ "day": "Tag", "days": "Tage", "deduplicate_all": "Alle Duplikate entfernen", - "deduplication_criteria_1": "BildgrÃļße in Bytes", - "deduplication_criteria_2": "Anzahl der EXIF-Daten", - "deduplication_info": "Deduplizierungsinformationen", - "deduplication_info_description": "FÃŧr die automatische Datei-Vorauswahl und das Deduplizieren aller Dateien berÃŧcksichtigen wir:", + "default_locale": "Standardgebietsschema", + "default_locale_description": "Datumsangaben und Zahlen werden entsprechend Ihrer Browsereinstellungen formatiert", "delete": "LÃļschen", "delete_action_confirmation_message": "Bist du sicher, dass du dieses Objekt lÃļschen willst? Diese Aktion wird das Objekt in den Papierkorb des Servers verschieben und fragen, ob du es lokal lÃļschen willst", "delete_action_prompt": "{count} gelÃļscht", @@ -970,7 +972,7 @@ "downloading_media": "Medien werden heruntergeladen", "drop_files_to_upload": "Lade Dateien hoch, indem du sie hierhin ziehst", "duplicates": "Duplikate", - "duplicates_description": "LÃļse jede Gruppe auf, indem du angibst, welche, wenn Ãŧberhaupt, Duplikate sind", + "duplicates_description": "LÃļse jede Gruppe auf, indem du angibst, welche, wenn Ãŧberhaupt, Duplikate sind.", "duration": "Dauer", "edit": "Bearbeiten", "edit_album": "Album bearbeiten", @@ -1024,7 +1026,7 @@ "enabled": "Aktiviert", "end_date": "Enddatum", "enqueued": "Eingereiht", - "enter_wifi_name": "WiFi-Name eingeben", + "enter_wifi_name": "WLAN-Name eingeben", "enter_your_pin_code": "PIN-Code eingeben", "enter_your_pin_code_subtitle": "Gib deinen PIN-Code ein, um auf den gesperrten Ordner zuzugreifen", "error": "Fehler", @@ -1036,7 +1038,7 @@ "error_loading_partners": "Fehler beim Laden der Partner: {error}", "error_retrieving_asset_information": "Fehler beim Abruf der Dateiinformationen", "error_saving_image": "Fehler: {error}", - "error_tag_face_bounding_box": "Fehler beim Markieren des Gesichts - Begrenzungen kÃļnnen nicht abgerufen werden", + "error_tag_face_bounding_box": "Fehler beim Taggen des Gesichts - Begrenzungen kÃļnnen nicht abgerufen werden", "error_title": "Fehler - Etwas ist schief gelaufen", "error_while_navigating": "Fehler beim Navigieren zur Datei", "errors": { @@ -1081,9 +1083,9 @@ "something_went_wrong": "Ein Fehler ist eingetreten", "unable_to_add_album_users": "Benutzer konnten nicht zum Album hinzugefÃŧgt werden", "unable_to_add_assets_to_shared_link": "Datei konnte nicht zum geteilten Link hinzugefÃŧgt werden", - "unable_to_add_comment": "Es kann kein Kommentar hinzufÃŧgt werden", + "unable_to_add_comment": "Es kann kein Kommentar hinzugefÃŧgt werden", "unable_to_add_exclusion_pattern": "Ausschlussmuster konnte nicht hinzugefÃŧgt werden", - "unable_to_add_partners": "Es kÃļnnen keine Partner hinzufÃŧgt werden", + "unable_to_add_partners": "Es kÃļnnen keine Partner hinzugefÃŧgt werden", "unable_to_add_remove_archive": "Datei konnte nicht {archived, select, true {aus dem Archiv entfernt} other {zum Archiv hinzugefÃŧgt}} werden", "unable_to_add_remove_favorites": "Datei konnte nicht {favorite, select, true {von den Favoriten entfernt} other {zu den Favoriten hinzugefÃŧgt}} werden", "unable_to_archive_unarchive": "Konnte nicht {archived, select, true {archivieren} other {entarchivieren}}", @@ -1240,7 +1242,7 @@ "geolocation_instruction_location": "Klicke auf eine Datei mit GPS Koordinaten um diesen Standort zu verwenden oder wähle einen Standort direkt auf der Karte", "get_help": "Hilfe erhalten", "get_people_error": "Fehler beim Laden der Personen", - "get_wifiname_error": "WiFi-Name konnte nicht ermittelt werden. Vergewissere dich, dass die erforderlichen Berechtigungen erteilt wurden und du mit einem WiFi-Netzwerk verbunden bist", + "get_wifiname_error": "Das WLAN-Netz konnte nicht ermittelt werden. Vergewissere dich, dass die erforderlichen Berechtigungen erteilt wurden und du mit einem WLAN-Netzwerk verbunden bist", "getting_started": "Erste Schritte", "go_back": "ZurÃŧck", "go_to_folder": "Gehe zu Ordner", @@ -1279,7 +1281,7 @@ "home_page_add_to_album_err_local": "Es kÃļnnen lokale Elemente noch nicht zu Alben hinzugefÃŧgt werden, Ãŧberspringen", "home_page_add_to_album_success": "{added} Elemente zu {album} hinzugefÃŧgt.", "home_page_album_err_partner": "Inhalte von Partnern kÃļnnen derzeit nicht zu Alben hinzugefÃŧgt werden", - "home_page_archive_err_local": "Kann lokale Elemente nicht archvieren, Ãŧberspringen", + "home_page_archive_err_local": "Kann lokale Elemente nicht archivieren, Ãŧberspringen", "home_page_archive_err_partner": "Inhalte von Partnern kÃļnnen nicht archiviert werden", "home_page_building_timeline": "Zeitachse wird erstellt", "home_page_delete_err_partner": "Inhalte von Partnern kÃļnnen nicht gelÃļscht werden, Ãŧberspringe", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "Titel des Albums", "licenses": "Lizenzen", "light": "Hell", + "light_theme": "Auf helle Ansicht umschalten", "like": "Gefällt mir", "like_deleted": "Like gelÃļscht", "link_motion_video": "Bewegungsvideo verknÃŧpfen", + "link_to_docs": "Weitere Informationen finden Sie in der Dokumentation.", "link_to_oauth": "Mit OAuth verknÃŧpfen", "linked_oauth_account": "VerknÃŧpftes OAuth-Konto", "list": "Liste", @@ -1404,7 +1408,7 @@ "local_network_sheet_info": "Die App stellt Ãŧber diese URL eine Verbindung zum Server her, wenn sie das angegebene WLAN-Netzwerk verwendet", "location": "Standort", "location_permission": "Standort Genehmigung", - "location_permission_content": "Um die automatische Umschaltfunktion nutzen zu kÃļnnen, benÃļtigt Immich genaue Standortberechtigung, damit es den Namen des aktuellen WiFi-Netzwerks ermitteln kann", + "location_permission_content": "Um die automatische Umschaltfunktion nutzen zu kÃļnnen, benÃļtigt Immich genaue Standortberechtigung, damit es den Namen des aktuellen WLAN-Netzwerks ermitteln kann", "location_picker_choose_on_map": "Auf der Karte auswählen", "location_picker_latitude_error": "GÃŧltigen Breitengrad eingeben", "location_picker_latitude_hint": "Breitengrad eingeben", @@ -1562,7 +1566,7 @@ "name_or_nickname": "Name oder Nickname", "name_required": "Name ist erforderlich", "navigate": "Navigation", - "navigate_to_time": "Navigiere zu Zeit", + "navigate_to_time": "Zu Zeitpunkt navigieren", "network_requirement_photos_upload": "Mobile Daten verwenden, um Fotos zu sichern", "network_requirement_videos_upload": "Mobile Daten verwenden, um Videos zu sichern", "network_requirements": "Anforderungen ans Netzwerk", @@ -1665,7 +1669,7 @@ "other_devices": "Andere Geräte", "other_entities": "Andere Entitäten", "other_variables": "Sonstige Variablen", - "owned": "Eigenes", + "owned": "Eigene", "owner": "Besitzer", "page": "Seite", "partner": "Partner", @@ -1872,7 +1876,7 @@ "repair": "Reparatur", "repair_no_results_message": "Nicht auffindbare und fehlende Dateien werden hier angezeigt", "replace_with_upload": "Durch Upload ersetzen", - "repository": "Repositorium", + "repository": "Repository", "require_password": "Passwort erforderlich", "require_user_to_change_password_on_first_login": "Benutzer muss das Passwort beim ersten Login ändern", "rescan": "Erneut scannen", @@ -2011,7 +2015,7 @@ "selected_count": "{count, plural, other {# ausgewählt}}", "selected_gps_coordinates": "Ausgewählte GPS-Koordinaten", "send_message": "Nachricht senden", - "send_welcome_email": "BegrÃŧssungsmail senden", + "send_welcome_email": "BegrÃŧßungsmail senden", "server_endpoint": "Server-Endpunkt", "server_info_box_app_version": "App-Version", "server_info_box_server_url": "Server-URL", @@ -2171,7 +2175,7 @@ "sort_people_by_similarity": "Personen nach Ähnlichkeit sortieren", "sort_recent": "Neuestes Foto", "sort_title": "Titel", - "source": "Quellcode", + "source": "Quelle", "stack": "Stapel", "stack_action_prompt": "{count} gestapelt", "stack_duplicates": "Duplikate stapeln", @@ -2213,6 +2217,7 @@ "tag": "Tag", "tag_assets": "Dateien taggen", "tag_created": "Tag erstellt: {tag}", + "tag_face": "Gesicht taggen", "tag_feature_description": "Durchsuchen von Fotos und Videos, gruppiert nach logischen Tag-Themen", "tag_not_found_question": "Kein Tag vorhanden? Erstelle einen neuen Tag.", "tag_people": "Personen taggen", @@ -2316,7 +2321,7 @@ "untagged": "Ohne Tag", "untitled_workflow": "Unbenannter Workflow", "up_next": "Weiter", - "update_location_action_prompt": "Aktualsiere den Ort von {count} ausgewählten Dateien mit:", + "update_location_action_prompt": "Aktualisiere den Ort von {count} ausgewählten Dateien mit:", "updated_at": "Aktualisiert", "updated_password": "Passwort aktualisiert", "upload": "Hochladen", @@ -2339,7 +2344,7 @@ "url": "URL", "usage": "Verwendung", "use_biometric": "Biometrie verwenden", - "use_browser_locale": "Benutze lokalen Browser", + "use_browser_locale": "Gebietsschema des Browsers verwenden", "use_browser_locale_description": "Datum, Uhrzeit und Zahlen werden entsprechend den Einstellungen Ihres Browsers formatiert", "use_current_connection": "Aktuelle Verbindung verwenden", "use_custom_date_range": "Stattdessen einen benutzerdefinierten Datumsbereich verwenden", @@ -2394,6 +2399,7 @@ "viewer_remove_from_stack": "Aus Stapel entfernen", "viewer_stack_use_as_main_asset": "An Stapelanfang", "viewer_unstack": "Stapel aufheben", + "visibility": "Sichtbarkeit", "visibility_changed": "Sichtbarkeit fÃŧr {count, plural, one {# Person} other {# Personen}} geändert", "visual": "Visuell", "visual_builder": "Visueller Editor", @@ -2404,7 +2410,7 @@ "welcome": "Willkommen", "welcome_to_immich": "Willkommen bei Immich", "width": "Breite", - "wifi_name": "WiFi-Name", + "wifi_name": "WLAN-Netzwerk", "workflow_delete_prompt": "Bist du sicher, dass du diesen Workflow lÃļschen willst?", "workflow_deleted": "Workflow gelÃļscht", "workflow_description": "Workflow-Beschreibung", @@ -2423,7 +2429,7 @@ "years_ago": "Vor {years, plural, one {einem Jahr} other {# Jahren}}", "yes": "Ja", "you_dont_have_any_shared_links": "Du hast keine geteilten Links", - "your_wifi_name": "Dein WiFi-Name", + "your_wifi_name": "Dein WLAN-Netzwerk", "zero_to_clear_rating": "drÃŧcke 0 um die Dateibewertung zurÃŧckzusetzen", "zoom_image": "Bild vergrÃļßern", "zoom_to_bounds": "Auf Grenzen zoomen" diff --git a/i18n/de_CH.json b/i18n/de_CH.json index 5d25d2a142..f4fd0c59de 100644 --- a/i18n/de_CH.json +++ b/i18n/de_CH.json @@ -1,132 +1,132 @@ { "about": "Über", "account": "Konto", - "account_settings": "Konto Istelligä", - "acknowledge": "Bestätige", + "account_settings": "Konto Einstellungen", + "acknowledge": "Bestätigä", "action": "Aktion", "action_common_update": "Update", - "action_description": "Es paar Aktione, wo a de gfilterete Assets usgfÃŧhrt wärde sÃļlled", - "actions": "Aktione", + "action_description": "Aktionä, wo uf de gefilterti Mediä ausgfÃŧhrt werdä solled", + "actions": "Aktionen", "active": "Aktiv", - "active_count": "Aktivi: {count}", + "active_count": "Aktiv: {count}", "activity": "Aktivität", - "activity_changed": "Aktivität isch {enabled, select, true {aktiviert} other {deaktiviert}}", - "add": "HinzuefÃŧegä", - "add_a_description": "Beschriibig hinzuefÃŧege", - "add_a_location": "Standort hinzuefÃŧege", - "add_a_name": "Name hinzuefÃŧege", - "add_a_title": "Titel hinzuefÃŧege", - "add_action": "Aktion hinzuefÃŧege", - "add_action_description": "Aklicke um en Aktion dure zfÃŧehre", - "add_assets": "Assets hinzufÃŧege", - "add_birthday": "Geburtstag hinzuefÃŧege", + "activity_changed": "Aktivität ist {enabled, select, true {aktiviert} other {deaktiviert}}", + "add": "HinzuefÃŧge", + "add_a_description": "Beschreibung hinzufÃŧgen", + "add_a_location": "Standort hinzuefÃŧgä", + "add_a_name": "Namä hinzefÃŧgä", + "add_a_title": "Titel hinzufeÃŧgä", + "add_action": "Aktion hinzuefÃŧgä", + "add_action_description": "Klick do zum e Aktion hinzuefÃŧge", + "add_assets": "Mediä hinzuefÃŧge", + "add_birthday": "Geburtstag hinzuefÃŧge", "add_endpoint": "Endpunkt hinzuefÃŧge", - "add_exclusion_pattern": "Uuschlussmuster hinzuefÃŧege", - "add_filter": "Filter hinzuefÃŧge", - "add_filter_description": "Klicke, um e Filterbedingig hinzuezfÃŧege", - "add_location": "Standort hinzuefÃŧege", - "add_more_users": "Meh Benutzer hinzuefÃŧege", - "add_partner": "Partner hinzuefÃŧege", - "add_path": "Pfad hinzuefÃŧege", - "add_photos": "FÃļteli hinzuefÃŧege", - "add_tag": "Tag hinzuefÃŧege", - "add_to": "HinzuefÃŧege zu â€Ļ", - "add_to_album": "Zum Album hinzuefÃŧege", - "add_to_album_bottom_sheet_added": "Zu {album} hinzuegfÃŧegt", - "add_to_album_bottom_sheet_already_exists": "Scho in {album}", - "add_to_album_bottom_sheet_some_local_assets": "Es hend es paar lokali Dateie nÃļd chÃļne im Album hinzuegfÃŧegt werde", - "add_to_album_toggle": "Uuswahl umschalte fÃŧr {album}", - "add_to_albums": "Zu Albe hinzuefÃŧege", - "add_to_albums_count": "Zu Albe hinzuefÃŧege ({count})", - "add_to_bottom_bar": "HinzuefÃŧege zu", - "add_to_shared_album": "Zum teilte Album hinzuefÃŧege", - "add_upload_to_stack": "Upload zum Stack hinzuefÃŧege", - "add_url": "URL hinzuefÃŧege", - "add_workflow_step": "Workflow-Schritt hinzuefÃŧege", - "added_to_archive": "Is Archiv verschobe", - "added_to_favorites": "Zu dine Favoritä hinzuegfÃŧegt", - "added_to_favorites_count": "{count, number} zu Favorite hinzuegfÃŧegt", + "add_exclusion_pattern": "Ausschlussmuster hinzufÃŧgen", + "add_filter": "Filter hinzufÃŧgen", + "add_filter_description": "Klicke hier um eine Filterbedingung hinzuzufÃŧgen", + "add_location": "Standort hinzufÃŧgen", + "add_more_users": "Mehr Benutzer hinzufÃŧgen", + "add_partner": "Partner hinzufÃŧgen", + "add_path": "Pfad hinzufÃŧgen", + "add_photos": "Fotos hinzufÃŧgen", + "add_tag": "Tag hinzufÃŧgen", + "add_to": "HinzufÃŧgen zuâ€Ļ", + "add_to_album": "Zu Album hinzufÃŧgen", + "add_to_album_bottom_sheet_added": "Zu {album} hinzugefÃŧgt", + "add_to_album_bottom_sheet_already_exists": "Bereits in {album}", + "add_to_album_bottom_sheet_some_local_assets": "Einige lokale Dateien konnten nicht zum Album hinzugefÃŧgt werden", + "add_to_album_toggle": "Auswahl umschalten fÃŧr {album}", + "add_to_albums": "Zu Alben hinzufÃŧgen", + "add_to_albums_count": "Zu Alben hinzufÃŧgen ({count})", + "add_to_bottom_bar": "HinzufÃŧgen zu", + "add_to_shared_album": "Zu geteiltem Album hinzufÃŧgen", + "add_upload_to_stack": "Upload zum Stapel hinzufÃŧgen", + "add_url": "URL hinzufÃŧgen", + "add_workflow_step": "Workflow-Schritt hinzufÃŧgen", + "added_to_archive": "Zum Archiv hinzugefÃŧgt", + "added_to_favorites": "Zu Favoriten hinzugefÃŧgt", + "added_to_favorites_count": "{count, number} zu Favoriten hinzugefÃŧgt", "admin": { - "add_exclusion_pattern_description": "Uusschlussmuster hinzuefÃŧge. Platzhalter, wie *, **, und ? wärded understÃŧtzt. Zum all Dateie i eim Verzeichnis namens „Raw\" ignoriere, „**/Raw/**“ verwände. Zum all Dateien ignorieren, wo uf „.tif“ änded, „**/*.tif“ verwände. Zum en absolute Pfad ignoriere, „/pfad/zum/ignoriere/**“ verwände.", - "admin_user": "Admin Benutzer", - "asset_offline_description": "Die Datei vonere externe Bibliothek isch nÃŧmme uf de Festplatte und isch in Papierchorb verschobe worde. Falls die Datei innerhalb vo de Bibliothek verschoben worde isch, ÃŧberprÃŧf dini Ziitleiste uf die neui entsprechendi Datei. Zum die Datei wiederherstelle, stell bitte sicher, dass Immich uf de unde stehendi Dateipfad chan zuegriife und scann d'Bibliothek.", - "authentication_settings": "Authentifizierigs Iistellige", - "authentication_settings_description": "Passwort, OAuth und anderi Authentifizierigseinstellige verwalte", - "authentication_settings_disable_all": "Bisch sicher, dass du alli Login-Methodä wotsch deaktivierä? S Login isch denn komplett deaktiviert.", - "authentication_settings_reenable": "Bruuch ein Server-Befehl zum reaktiviere.", - "background_task_job": "Hintergrund Ufgabä", - "backup_database": "Datenbank-Dump aalege", - "backup_database_enable_description": "Datenbank-Dumps aktiviere", - "backup_keep_last_amount": "Aazahl vo de vorherige Dumps, wo bhalte werde sÃļlle", - "backup_onboarding_1_description": "Offsite-Kopie i dä Cloud oder amene andere physische Standort.", - "backup_onboarding_2_description": "Lokali Kopie uf verschiedene Grät. Das beinhaltet d Hauptdateie und e lokali Sicherig vo dene Dateie.", - "backup_onboarding_3_description": "Total aazahl vo dine Dateikopie, inklusiv d Originaldateie. Das beinhaltet 1 Offsite-Kopie und 2 lokali Kopie.", - "backup_onboarding_description": "E 3-2-1-Backup-Strategie wird empfohle, zum dini Dateie z schÃŧtze. Du sÃļttsch sowohl Kopie vo dine ufgeladene Fotos/Videos wie au d Immich-Datenbank bhalte, fÃŧr e rundum sauberi Backup-LÃļsig.", - "backup_onboarding_footer": "FÃŧr meh Infos zum Backup vo Immich lueg bitte i d Dokumentation.", - "backup_onboarding_parts_title": "Es 3-2-1-Backup beinhaltet:", + "add_exclusion_pattern_description": "Ausschlussmuster hinzufÃŧgen. Platzhalter, wie *, **, und ? werden unterstÃŧtzt. Um alle Dateien in einem Verzeichnis namens „Raw“ zu ignorieren, „**/Raw/**“ verwenden. Um alle Dateien zu ignorieren, die auf „.tif“ enden, „**/*.tif“ verwenden. Um einen absoluten Pfad zu ignorieren, „/pfad/zum/ignorieren/**“ verwenden.", + "admin_user": "Administrator", + "asset_offline_description": "Diese Datei einer externen Bibliothek befindet sich nicht mehr auf der Festplatte und wurde in den Papierkorb verschoben. Falls die Datei innerhalb der Bibliothek verschoben wurde, ÃŧberprÃŧfe deine Zeitleiste auf die neue entsprechende Datei. Um diese Datei wiederherzustellen, stelle bitte sicher, dass Immich auf den unten stehenden Dateipfad zugreifen kann und scanne die Bibliothek.", + "authentication_settings": "Authentifizierungseinstellungen", + "authentication_settings_description": "Passwort-, OAuth- und andere Authentifizierungseinstellungen verwalten", + "authentication_settings_disable_all": "Bist du sicher, dass du alle Loginmethoden deaktivieren willst? Die Anmeldung wird vollständig deaktiviert.", + "authentication_settings_reenable": "Nutze einen Server-Befehl zur Reaktivierung.", + "background_task_job": "Hintergrundaufgaben", + "backup_database": "Datenbanksicherung erstellen", + "backup_database_enable_description": "Datenbank regelmässig sichern", + "backup_keep_last_amount": "Anzahl der aufzubewahrenden frÃŧheren Sicherungen", + "backup_onboarding_1_description": "Offsite-Kopie in der Cloud oder an einem anderen physischen Ort.", + "backup_onboarding_2_description": "lokale Kopien auf verschiedenen Geräten. Dazu gehÃļren die Hauptdateien und eine lokale Sicherung dieser Dateien.", + "backup_onboarding_3_description": "Kopien deiner Daten inklusive Originaldateien. Dies umfasst 1 Kopie an einem anderen Ort und 2 lokale Kopien.", + "backup_onboarding_description": "Eine 3-2-1 Sicherungsstrategie wird empfohlen, um deine Daten zu schÃŧtzen. Du solltest sowohl Kopien deiner hochgeladenen Fotos/Videos als auch der Immich-Datenbank aufbewahren, um eine umfassende SicherungslÃļsung zu haben.", + "backup_onboarding_footer": "Weitere Informationen zum Sichern von Immich findest du in der Dokumentation.", + "backup_onboarding_parts_title": "Eine 3-2-1-Sicherung umfasst:", "backup_onboarding_title": "Backups", - "backup_settings": "Iistellige fÃŧr Datenbank-Dumps", - "backup_settings_description": "Datenbank-Dump-Iistellige verwalte.", - "cleared_jobs": "Jobs glÃļscht fÃŧr: {job}", - "config_set_by_file": "D Konfiguration isch aktuell dur e Konfigurationsdatei gsetzt", - "confirm_delete_library": "Bisch sicher, dass du d Bibliothek {library} wotsch lÃļsche?", - "confirm_delete_library_assets": "Bisch sicher, dass du die Bibliothek wotsch lÃļsche? Das lÃļscht {count, plural, one {# enthaltenes Asset} other {alli # enthaltene Assets}} us Immich und chan nÃļd rÃŧckgängig gmacht werde. D Dateie bliibed uf em Dateträger.", - "confirm_email_below": "Zum bestätige bitte \"{email}\" une iitippe", - "confirm_reprocess_all_faces": "Bisch sicher, dass du alli Gsichter neu verarbeite wotsch? Däbii werde au benannti Persone glÃļscht.", - "confirm_user_password_reset": "Bisch sicher, dass du s Passwort fÃŧr {user} mÃļchtisch zruggsetze?", - "confirm_user_pin_code_reset": "Bisch sicher, dass du de PIN-Code vo {user} mÃļchtisch zruggsetze?", - "copy_config_to_clipboard_description": "Kopier die aktuelli Systemkonfiguration als JSON-Objekt i d'ZwÃŧschenablage", - "create_job": "Uufgabe erstelle", - "cron_expression": "Cron-Ziitagabe", - "cron_expression_description": "Setz s Scanintervall im Cron-Format. Hilf mit däm Format bÃŧtet z. B. der Crontab Guru", - "cron_expression_presets": "Vorlage fÃŧr Cron-Uusdruck", - "disable_login": "Login deaktiviere", - "duplicate_detection_job_description": "Die Uufgab fÃŧehrt s maschinelle Lärne fÃŧr jedi Datei us, zum Duplikat finde. Die Uufgabe berueht uf de intelligente Suechi", - "exclusion_pattern_description": "Mit Uusschlussmuster chÃļnnd Dateie und Ordner bim Scanne vo dinere Bibliothek ignoriert wärde. Das isch nÃŧtzlich, wenn du Ordner häsch, wo Dateien drin händ, wo d nÃļd wotsch importiere, wie z. B. RAW-Dateie.", - "export_config_as_json_description": "Lad die aktuelli Systemkonfiguration als JSON-Datei abe", - "external_libraries_page_description": "Externi Bibliothekssiite fÃŧr Administratore", - "face_detection": "Gsichtserkennig", - "face_detection_description": "Die Uufgab erfasst Gsichter in Dateien dur maschinells Lerne. Bi Video wird nur d'Miniaturasicht brucht. „Aktualisiere“ verarbeitet all Dateie neu. „Zruggsetze“ setzt au no all Gsichter zrugg. „Fehlendi“ stellt nur nÃļd verarbeiteti Dateie in d'Warteschlange. Erfassti Gsichter wärdet zur Gsichtsidentifizierig in diWarteschlange gstellt, damit sie i bestehendi oder neui Persone z'gruppiere.", - "facial_recognition_job_description": "Die Uufgabe gruppiert im Anschluss an d'Gsichtserfassig die erfasste Gsichter zu Persone. „Zruggsetze“ gruppiert alli Gsichter neu und mit „Fehlendi“ werdet Gsichter ohni Zuordnig i d'Warteschlange gstellt.", - "failed_job_command": "Befehl {command} hät fÃŧr d'Uufgabe {job} nÃļd funktioniert", - "force_delete_user_warning": "WARNIG: Die Aktion lÃļscht dä Benutzer und all sini Dateie. Das chann nÃļd rÃŧckgängig gmacht wärde und d'Dateie chÃļnnd nÃļd wiederhergstellt wärde.", + "backup_settings": "Einstellungen fÃŧr Datenbanksicherung", + "backup_settings_description": "Einstellungen zur regelmässigen Sicherung der Datenbank.", + "cleared_jobs": "Folgende Aufgaben zurÃŧckgesetzt: {job}", + "config_set_by_file": "Die Konfiguration ist aktuell durch eine Konfigurationsdatei gsetzt", + "confirm_delete_library": "Bist du sicher, dass du die Bibliothek {library} lÃļschen willst?", + "confirm_delete_library_assets": "Bist du sicher, dass du diese Bibliothek lÃļschen willst? Dies lÃļscht {count, plural, one {# enthaltene Datei} other {alle # enthaltenen Dateien}} aus Immich und kann nicht rÃŧckgängig gemacht werden. Die Dateien bleiben auf der Festplatte erhalten.", + "confirm_email_below": "Zum Bestätigen, tippe unten \"{email}\" ein", + "confirm_reprocess_all_faces": "Bist du sicher, dass du alle Gesichter erneut verarbeiten mÃļchtest? Dies lÃļscht auch alle bereits benannten Personen.", + "confirm_user_password_reset": "Bist du sicher, dass du das Passwort fÃŧr {user} zurÃŧcksetzen mÃļchtest?", + "confirm_user_pin_code_reset": "Bist du sicher, dass du den PIN-Code von {user} zurÃŧcksetzen mÃļchtest?", + "copy_config_to_clipboard_description": "Aktuelle Systemkonfiguration als JSON-Objekt in die Zwischenablage kopieren", + "create_job": "Aufgabe erstellen", + "cron_expression": "Cron-Ausdruck", + "cron_expression_description": "Setze das Scanintervall im Cron-Format. FÃŧr mehr Informationen, siehe z. B. Crontab Guru", + "cron_expression_presets": "Vorlagen fÃŧr Cron-AusdrÃŧcke", + "disable_login": "Login deaktivieren", + "duplicate_detection_job_description": "Verwendet maschinelles Lernen auf den Dateien, um Duplikate zu finden. Baut auf der intelligenten Suche auf", + "exclusion_pattern_description": "Mit Ausschlussmustern kÃļnnen Dateien und Ordner beim Scannen deiner Bibliothek ignoriert werden. Dies ist nÃŧtzlich, wenn du Ordner hast, die Dateien enthalten, die du nicht importieren mÃļchtest, wie z. B. RAW-Dateien.", + "export_config_as_json_description": "Aktuelle Systemkonfiguration als JSON-Datei herunterladen", + "external_libraries_page_description": "Externe Bibliotheksseite fÃŧr Administratoren", + "face_detection": "Gesichtserkennung", + "face_detection_description": "Diese Aufgabe erkennt mit maschinellem Lernen Gesichter in Dateien. Bei Videos wird nur das Vorschaubild verwendet. „Aktualisieren“ verarbeitet alle Dateien neu. „ZurÃŧcksetzen“ setzt zusätzlich alle Gesichter zurÃŧck. „Fehlende“ fÃŧgt nur nicht verarbeitete Dateien in die Warteschlange ein. Erfasste Gesichter werden zur Gesichtsidentifizierung in die Warteschlange eingefÃŧgt, um sie in bestehende oder neue Personen zu gruppieren.", + "facial_recognition_job_description": "Diese Aufgabe gruppiert im Anschluss an die Gesichtserkennung die erkannten Gesichter zu Personen. „ZurÃŧcksetzen“ gruppiert alle Gesichter neu, während „Fehlende“ Gesichter ohne Zuordnung in die Warteschlange stellt.", + "failed_job_command": "Befehl {command} ist fÃŧr Aufgabe {job} fehlgeschlagen", + "force_delete_user_warning": "WARNUNG: Diese Aktion lÃļscht sofort den Benutzer und all seine Dateien. Dies kann nicht rÃŧckgängig gemacht werden und die Dateien kÃļnnen nicht wiederhergestellt werden.", "image_format": "Format", - "image_format_description": "WebP erzeugt chlineri Dateie we JPEG, isch aber es bitz langsamer i de Erstellig.", - "image_fullsize_description": "HochuflÃļsends Bild mit glÃļschte Metadate, wo bim Zoome brucht wird", - "image_fullsize_enabled": "HochuflÃļsendi Vorschaubilder aktiviere", - "image_fullsize_enabled_description": "Generiere hochauflÃļsende Vorschaubilder in OriginalauflÃļsung fÃŧr nicht web-kompatibel Formate. Wenn \"Eingebettete Vorschau bevorzugen\" aktiviert ist, werden eingebettete Vorschaubilder direkt verwendet. Hat keinen Einfluss auf web-kompatible Formate wie JPEG.", + "image_format_description": "WebP erzeugt kleinere Dateien als JPEG, ist aber etwas langsamer in der Erstellung.", + "image_fullsize_description": "HochauflÃļsendes Bild mit entfernten Metadaten, das beim Zoomen verwendet wird", + "image_fullsize_enabled": "HochauflÃļsende Vorschaubilder aktivieren", + "image_fullsize_enabled_description": "Generiere Vorschaubilder in OriginalauflÃļsung fÃŧr nicht web-kompatible Formate. Wenn \"Eingebettete Vorschau bevorzugen\" aktiviert ist, werden eingebettete Vorschaubilder direkt verwendet. Hat keinen Einfluss auf web-kompatible Formate wie JPEG.", "image_fullsize_quality_description": "Qualität der hochauflÃļsenden Vorschaubilder von 1-100. HÃļher ist besser, erzeugt aber grÃļssere Dateien.", "image_fullsize_title": "HochauflÃļsende Vorschaueinstellungen", "image_prefer_embedded_preview": "Eingebettete Vorschau bevorzugen", "image_prefer_embedded_preview_setting_description": "Verwende eingebettete Vorschaubilder in RAW-Fotos als Grundlage fÃŧr die Bildverarbeitung, sofern diese zur VerfÃŧgung stehen. Dies kann bei einigen Bildern genauere Farben erzeugen, allerdings ist die Qualität der Vorschau kameraabhängig und das Bild kann mehr Kompressionsartefakte aufweisen.", "image_prefer_wide_gamut": "Breites Spektrum bevorzugen", - "image_prefer_wide_gamut_setting_description": "Bruuch Display P3 fÃŧr Vorschaubildli. Das erhaltet d'Vitalität von Bildli mit grossem Farbruum besser. Uf alte Grät mit alte Browser chann das aber andersch uusgseh. sRGB-Bildli wärdet als sRGB bhalte zum Farbänderige vermiide.", - "image_preview_description": "Mittelgrossi Bildli ohni Metadate, bruuchts fÃŧr Einzelaasichte und fÃŧrs maschinelle Lärne", - "image_preview_quality_description": "Vorschauqualität vo 1-100. HÃļcher isch besser, git aber grÃļsseri Dateie und chan d'App Schwuppdizität reduziere. Z tÃŧffi Wert chÃļnnd s maschinelle Lärne beiträchtige.", - "image_preview_title": "Vorschauiistellige", + "image_prefer_wide_gamut_setting_description": "Display P3 (DCI-P3) fÃŧr Vorschaubilder verwenden. Dadurch bleibt die Lebendigkeit von Bildern mit breiten Farbräumen besser erhalten, aber die Bilder kÃļnnen auf älteren Geräten mit einer älteren Browserversion etwas anders aussehen. sRGB-Bilder werden im sRGB-Format belassen, um Farbverschiebungen zu vermeiden.", + "image_preview_description": "Mittelgrosses Bild mit entfernten Metadaten, das bei der Betrachtung einer einzelnen Datei und fÃŧr maschinelles Lernen verwendet wird", + "image_preview_quality_description": "Vorschauqualität von 1-100. Ein hÃļherer Wert ist besser, erzeugt dadurch aber grÃļssere Dateien und kann die Reaktionsfähigkeit der App beeinträchtigen. Ein niedriger Wert kann dafÃŧr aber die Qualität des maschinellen Lernens beeinträchtigen.", + "image_preview_title": "Vorschaueinstellungen", "image_progressive": "Fortlaufend", - "image_progressive_description": "Codier fortlaufendi JPEG-Bildi: Sie wärdet bim Lade aufbauend aazeiget. Das hät kei WÃŧrkig uf WebP-Bildi.", + "image_progressive_description": "JPEG-Bilder schrittweise kodieren, um ein stufenweises Laden zu ermÃļglichen. Dies hat keine Auswirkungen auf WebP-Bilder.", "image_quality": "Qualität", - "image_resolution": "UuflÃļsig", - "image_resolution_description": "HÃļcheri UuflÃļsig erhaltet meh Detail, gaht aber länger zum codiere, macht grÃļsseri Dateie und chan d'App Schuppdizität reduziere.", - "image_settings": "Bild-Iistellige", - "image_settings_description": "Qualität und UuflÃļsig von erstellte Bildli verwalte", - "image_thumbnail_description": "Chlini Vorschaubildli ohni Metadate, bruuchts fÃŧr Aasichte mit Gruppe vo FÃļteli wie i de Hauptziitachse", - "image_thumbnail_quality_description": "Vorschauqualität vo 1-100. HÃļcher isch besser, git aber grÃļsseri Dateie und chan d'App Schwuppdizität reduziere.", - "image_thumbnail_title": "Iistellige fÃŧr Vorschaubildli", - "import_config_from_json_description": "Systemkonfiguration importiere durs Ufelade vonere JSON-Datei", - "job_concurrency": "{job} Näbeläufigkeit", - "job_created": "Uufgab erstellt", - "job_not_concurrency_safe": "Die Uufgabe ist nÃļd fÃŧr ParalleluusfÃŧhrig gmacht.", - "job_settings": "Uufgabe-Iistellige", - "job_settings_description": "Uufgabe-Näbeläufigkeit verwalte", - "jobs_over_time": "Uufgabe in ziitliche Verlauf", + "image_resolution": "AuflÃļsung", + "image_resolution_description": "HÃļhere AuflÃļsungen kÃļnnen mehr Details erhalten, benÃļtigen aber mehr Zeit fÃŧr die Kodierung, haben grÃļssere DateigrÃļssen und kÃļnnen die Reaktionsfähigkeit der App beeinträchtigen.", + "image_settings": "Bildeinstellungen", + "image_settings_description": "Qualität und AuflÃļsung der generierten Bilder verwalten", + "image_thumbnail_description": "Kleines Vorschaubild mit entfernten Metadaten, die bei der Anzeige von Sammlungen von Fotos wie der Zeitleiste verwendet wird", + "image_thumbnail_quality_description": "Qualität der Vorschaubilder von 1-100. HÃļher ist besser, erzeugt aber grÃļssere Dateien und kann die Reaktionsfähigkeit der App beeinträchtigen.", + "image_thumbnail_title": "Einstellungen fÃŧr Vorschaubilder", + "import_config_from_json_description": "Systemkonfiguration von hochgeladener JSON-Konfigurationsdatei importieren", + "job_concurrency": "{job} (Anzahl gleichzeitig laufende Prozesse)", + "job_created": "Aufgabe erstellt", + "job_not_concurrency_safe": "Diese Aufgabe kann nicht mehrmals parallel laufen gelassen werden.", + "job_settings": "Aufgabeneinstellungen", + "job_settings_description": "Gleichzeitige AusfÃŧhrung von Aufgaben verwalten", + "jobs_over_time": "Jobs im Laufe der Zeit", "library_created": "Bibliothek erstellt: {library}", - "library_deleted": "Bibliothek glÃļscht", - "library_details": "Bibliotheks-Details", - "library_folder_description": "Gib en Order zum Importiere a. Dä Order mit sine Underordner wird nach Bildli und Videos durchsucht.", - "library_remove_exclusion_pattern_prompt": "Bisch sicher, dass das Uuschluss-Muster wotsch lÃļsche?", - "library_remove_folder_prompt": "Bisch sicher, dass dä Import-Ordner wotsch lÃļsche?", - "library_scanning": "Regelmässigi ÜberprÃŧefig" + "library_deleted": "Bibliothek gelÃļscht", + "library_details": "Bibliotheksdetails", + "library_folder_description": "Wähle einen Ordner zum Importieren. Dieser Ordner wird inklusive Unterordnern nach Bildern und Videos durchsucht.", + "library_remove_exclusion_pattern_prompt": "Bilst du sicher, dass du dieses Ausschlussmuster entfernen mÃļchtest?", + "library_remove_folder_prompt": "Bist du sicher, dass du diesen Import-Ordner entfernen mÃļchtest?", + "library_scanning": "Regelmässiges Scannen" } } diff --git a/i18n/el.json b/i18n/el.json index 851a4edb27..8cd20d04a4 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -441,7 +441,7 @@ "user_successfully_removed": "Ο Ī‡ĪÎŽĪƒĪ„ÎˇĪ‚ {email} ÎąĪ†ÎąÎšĪÎ­Î¸ÎˇÎēÎĩ ÎŧÎĩ ÎĩĪ€ÎšĪ„Ī…Ī‡Î¯Îą.", "users_page_description": "ÎŖÎĩÎģÎ¯Î´Îą Ī‡ĪÎˇĪƒĪ„ĪŽÎŊ Î´ÎšÎąĪ‡ÎĩÎšĪÎšĪƒĪ„ÎŽ", "version_check_enabled_description": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ ÎĩÎģÎ­ÎŗĪ‡ÎŋĪ… έÎēδÎŋĪƒÎˇĪ‚", - "version_check_implications": "Η ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯Îą ÎĩÎģÎ­ÎŗĪ‡ÎŋĪ… έÎēδÎŋĪƒÎˇĪ‚, ÎĩÎžÎąĪĪ„ÎŦĪ„ÎąÎš ÎąĪ€ĪŒ Ī„ÎˇÎŊ Ī€ÎĩĪÎšÎŋδΚÎēÎŽ ÎĩĪ€ÎšÎēÎŋΚÎŊΉÎŊÎ¯Îą ÎŧÎĩ Ī„Îŋ github.com", + "version_check_implications": "Η ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯Îą ÎĩÎģÎ­ÎŗĪ‡ÎŋĪ… έÎēδÎŋĪƒÎˇĪ‚, ÎĩÎžÎąĪĪ„ÎŦĪ„ÎąÎš ÎąĪ€ĪŒ Ī„ÎˇÎŊ Ī€ÎĩĪÎšÎŋδΚÎēÎŽ ÎĩĪ€ÎšÎēÎŋΚÎŊΉÎŊÎ¯Îą ÎŧÎĩ Ī„Îŋ {server}", "version_check_settings": "ΈÎģÎĩÎŗĪ‡ÎŋĪ‚ ÎĩÎēδÎŋĪƒÎˇĪ‚", "version_check_settings_description": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ/ÎąĪ€ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ Ī„ÎˇĪ‚ ÎĩΚδÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇĪ‚ ÎŗÎšÎą ÎŊέι έÎēδÎŋĪƒÎˇ", "video_conversion_job": "ΜÎĩĪ„ÎąĪ„ĪÎŋĪ€ÎŽ Î˛Î¯ÎŊĪ„ÎĩÎŋ", @@ -849,9 +849,12 @@ "create_link_to_share": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ĪƒĪ…ÎŊÎ´Î­ĪƒÎŧÎŋĪ… ÎŗÎšÎą δΚιÎŧÎŋÎšĪÎąĪƒÎŧΌ", "create_link_to_share_description": "Î•Ī€ÎšĪ„ĪÎ­ĪˆĪ„Îĩ ΃Îĩ ÎŋĪ€ÎŋΚÎŋÎŊÎ´ÎŽĪ€ÎŋĪ„Îĩ Î­Ī‡ÎĩΚ Ī„ÎŋÎŊ ĪƒĪÎŊδÎĩ΃ÎŧÎŋ ÎŊÎą δÎĩΚ Ī„Îˇ/Ī„ÎšĪ‚ ÎĩĪ€ÎšÎģÎĩÎŗÎŧέÎŊΡ/ÎĩĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯Îą/ÎĩĪ‚", "create_new": "ΔΗΜΙΟÎĨΡΓΙΑ ΝΕΟÎĨ", - "create_new_person": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎŊέÎŋĪ… ΀΁ÎŋĪƒĪŽĪ€ÎŋĪ…", + "create_new_face": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎŊέÎŋĪ… ΀΁ÎŋĪƒĪŽĪ€ÎŋĪ…", + "create_new_person": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎŊέÎŋĪ… ÎąĪ„ĪŒÎŧÎŋĪ…", "create_new_person_hint": "ΑÎŊĪ„ÎšĪƒĪ„ÎŋÎ¯Ī‡ÎšĪƒÎˇ ΄ΉÎŊ ÎĩĪ€ÎšÎģÎĩÎŗÎŧέÎŊΉÎŊ ÎąĪĪ‡ÎĩÎ¯Ī‰ÎŊ ΃Îĩ έÎŊÎą ÎŊέÎŋ Ī€ĪĪŒĪƒĪ‰Ī€Îŋ", "create_new_user": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎŊέÎŋĪ… Ī‡ĪÎŽĪƒĪ„Îˇ", + "create_person": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎąĪ„ĪŒÎŧÎŋĪ…", + "create_person_subtitle": "Î ĪÎŋĪƒÎ¸Î­ĪƒĪ„Îĩ έÎŊÎą ΌÎŊÎŋÎŧÎą ĪƒĪ„Îŋ ÎĩĪ€ÎšÎģÎĩÎŗÎŧέÎŊÎŋ Ī€ĪĪŒĪƒĪ‰Ī€Îŋ ÎŗÎšÎą ÎŊÎą δΡÎŧΚÎŋĪ…ĪÎŗÎˇÎ¸Îĩί ÎēιΚ ÎŊÎą ÎĩĪ€ÎšĪƒÎˇÎŧÎąÎŊθÎĩί Ī„Îŋ ÎŊέÎŋ ÎŦĪ„ÎŋÎŧÎŋ", "create_shared_album_page_share_add_assets": "Î ÎĄÎŸÎŖÎ˜Î—ÎšÎ— ÎŖÎ¤ÎŸÎ™Î§Î•Î™ÎŠÎ", "create_shared_album_page_share_select_photos": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ ÎĻΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚", "create_shared_link": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎēÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„ÎŋĪ… ĪƒĪ…ÎŊÎ´Î­ĪƒÎŧÎŋĪ…", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "ΔιÎŋĪÎ¸ĪŽÎ¸ÎˇÎēÎĩ", "crop_aspect_ratio_free": "ΕÎģÎĩĪÎ¸Îĩ΁Îŋ", "crop_aspect_ratio_original": "Î‘Ī…Î¸ÎĩÎŊĪ„ÎšÎēΌ", + "crop_aspect_ratio_square": "ΤÎĩ΄΁ÎŦÎŗĪ‰ÎŊÎŋ", "curated_object_page_title": "Î ĪÎŦÎŗÎŧÎąĪ„Îą", "current_device": "Î¤ĪÎ­Ī‡ÎŋĪ…ĪƒÎą ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ", "current_pin_code": "Î¤ĪÎ­Ī‡Ī‰ÎŊ ÎēĪ‰Î´ÎšÎēĪŒĪ‚ PIN", @@ -880,7 +884,7 @@ "daily_title_text_date": "Ε, MMM dd", "daily_title_text_date_year": "Ε, MMM dd, yyyy", "dark": "ÎŖÎēÎŋĪĪÎŋ", - "dark_theme": "ΕÎŊÎąÎģÎģÎąÎŗÎŽ ΃ÎēÎŋĪ„ÎĩΚÎŊÎŽĪ‚ ÎĩÎŧΆÎŦÎŊÎšĪƒÎˇĪ‚", + "dark_theme": "ΜÎĩĪ„ÎŦÎ˛ÎąĪƒÎˇ ΃Îĩ ΃ÎēÎŋĪ„ÎĩΚÎŊΌ θέÎŧÎą", "date": "ΗÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯Îą", "date_after": "ΗÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯Îą ÎŧÎĩĪ„ÎŦ", "date_and_time": "ΗÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯Îą ÎēιΚ ĪŽĪÎą", @@ -891,10 +895,8 @@ "day": "ΗÎŧÎ­ĪÎą", "days": "ΗÎŧÎ­ĪÎĩĪ‚", "deduplicate_all": "Î‘Ī†ÎąÎ¯ĪÎĩĪƒÎˇ ΌÎģΉÎŊ ΄ΉÎŊ Î´ÎšĪ€ÎģĪŒĪ„Ī…Ī€Ī‰ÎŊ", - "deduplication_criteria_1": "ÎœÎ­ÎŗÎĩθÎŋĪ‚ ÎĩΚÎēΌÎŊÎąĪ‚ ΃Îĩ byte", - "deduplication_criteria_2": "Î‘ĪÎšÎ¸ÎŧĪŒĪ‚ δÎĩδÎŋÎŧέÎŊΉÎŊ EXIF", - "deduplication_info": "ΠÎģÎˇĪÎŋΆÎŋĪÎ¯ÎĩĪ‚ Î‘Ī†ÎąÎ¯ĪÎĩĪƒÎˇĪ‚ Î”ÎšĪ€ÎģÎŋĪ„ĪĪ€Ī‰ÎŊ", - "deduplication_info_description": "Για ÎŊÎą ΀΁ÎŋÎĩĪ€ÎšÎģέΞÎŋĪ…ÎŧÎĩ ÎąĪ…Ī„ĪŒÎŧÎąĪ„Îą Ī„Îą ÎąĪĪ‡ÎĩÎ¯Îą ÎēιΚ ÎŊÎą ÎąĪ†ÎąÎšĪÎ­ĪƒÎŋĪ…ÎŧÎĩ Ī„Îą Î´ÎšĪ€ÎģĪŒĪ„Ī…Ī€Îą ΃Îĩ ÎŧÎąÎļΚÎēÎŽ ÎĩĪ€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą, ÎĩΞÎĩĪ„ÎŦÎļÎŋĪ…ÎŧÎĩ ΃Îĩ:", + "default_locale": "Î ĪÎŋÎĩĪ€ÎšÎģÎĩÎŗÎŧέÎŊΡ ÎŗÎģĪŽĪƒĪƒÎą", + "default_locale_description": "ΜÎŋ΁ΆÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ ΡÎŧÎĩ΁ÎŋÎŧΡÎŊÎšĪŽÎŊ ÎēιΚ ÎąĪÎšÎ¸ÎŧĪŽÎŊ, βÎŦĪƒÎˇ Ī„ÎˇĪ‚ ÎŗÎģĪŽĪƒĪƒÎąĪ‚ Ī„ÎŋĪ… ΀΁ÎŋÎŗĪÎŦÎŧÎŧÎąĪ„ÎŋĪ‚ Ī€ÎĩĪÎšÎŽÎŗÎˇĪƒÎˇĪ‚", "delete": "Î”ÎšÎąÎŗĪÎąĪ†ÎŽ", "delete_action_confirmation_message": "Î•Î¯ĪƒĪ„Îĩ ĪƒÎ¯ÎŗÎŋ΅΁ÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą Î´ÎšÎąÎŗĪÎŦΈÎĩĪ„Îĩ ÎąĪ…Ī„ĪŒ Ī„Îŋ ÎąĪĪ‡ÎĩίÎŋ; Î‘Ī…Ī„ÎŽ Ρ ÎĩÎŊÎ­ĪÎŗÎĩΚι θι Ī„Îŋ ÎŧÎĩĪ„ÎąÎēΚÎŊÎŽĪƒÎĩΚ ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ Ī„ÎŋĪ… δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ ÎēιΚ θι ÎĩÎŧĪ†ÎąÎŊÎšĪƒĪ„Îĩί ÎŧÎŽÎŊĪ…ÎŧÎą ÎŗÎšÎą Ī„Îŋ ÎąÎŊ θέÎģÎĩĪ„Îĩ ÎŊÎą Ī„Îŋ Î´ÎšÎąÎŗĪÎŦΈÎĩĪ„Îĩ ÎēιΚ Ī„ÎŋĪ€ÎšÎēÎŦ", "delete_action_prompt": "{count} Î´ÎšÎąÎŗĪÎŦĪ†ÎˇÎēÎąÎŊ", @@ -970,7 +972,7 @@ "downloading_media": "Î›ÎŽĪˆÎˇ Ī€ÎŋÎģĪ…ÎŧÎ­ĪƒĪ‰ÎŊ", "drop_files_to_upload": "ÎŖĪĪÎĩĪ„Îĩ ÎąĪĪ‡ÎĩÎ¯Îą ÎĩÎ´ĪŽ ÎŗÎšÎą ÎŊÎą Ī„Îą ÎąÎŊÎĩβÎŦ΃ÎĩĪ„Îĩ", "duplicates": "Î”ÎšĪ€ÎģĪŒĪ„Ī…Ī€Îą", - "duplicates_description": "Î•Ī€ÎšÎģĪĪƒĪ„Îĩ ÎēÎŦθÎĩ ÎŋÎŧÎŦδι Ī…Ī€ÎŋδÎĩΚÎēÎŊĪÎŋÎŊĪ„ÎąĪ‚ Ī€ÎŋΚÎĩĪ‚ ÎĩίÎŊιΚ Î´ÎšĪ€ÎģĪŒĪ„Ī…Ī€ÎĩĪ‚, ÎĩÎŦÎŊ Ī…Ī€ÎŦ΁·ÎŋĪ…ÎŊ", + "duplicates_description": "Î•Ī€ÎšÎģĪĪƒĪ„Îĩ ÎēÎŦθÎĩ ÎŋÎŧÎŦδι Ī…Ī€ÎŋδÎĩΚÎēÎŊĪÎŋÎŊĪ„ÎąĪ‚ Ī€ÎŋΚÎĩĪ‚, ÎĩÎŦÎŊ Ī…Ī€ÎŦ΁·ÎŋĪ…ÎŊ, ÎĩίÎŊιΚ Î´ÎšĪ€ÎģĪŒĪ„Ī…Ī€ÎĩĪ‚.", "duration": "ΔιÎŦ΁ÎēÎĩΚι", "edit": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą", "edit_album": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "Î¤Î¯Ī„ÎģÎŋĪ‚ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "licenses": "ΆδÎĩΚÎĩĪ‚", "light": "ÎĻΉ΄ÎĩΚÎŊΌ", + "light_theme": "ΜÎĩĪ„ÎŦÎ˛ÎąĪƒÎˇ ΃Îĩ ΆΉ΄ÎĩΚÎŊΌ θέÎŧÎą", "like": "ΜÎŋĪ… ÎąĪÎ­ĪƒÎĩΚ", "like_deleted": "ΤÎŋ \"ÎŧÎŋĪ… ÎąĪÎ­ĪƒÎĩΚ\" Î´ÎšÎąÎŗĪÎŦĪ†ÎˇÎēÎĩ", "link_motion_video": "ÎŖĪÎŊδÎĩ΃Îĩ Î˛Î¯ÎŊĪ„ÎĩÎŋ ÎēίÎŊÎˇĪƒÎˇĪ‚", + "link_to_docs": "Για Ī€ÎĩĪÎšĪƒĪƒĪŒĪ„Îĩ΁ÎĩĪ‚ Ī€ÎģÎˇĪÎŋΆÎŋĪÎ¯ÎĩĪ‚, ÎąÎŊÎąĪ„ĪÎ­ÎžĪ„Îĩ ĪƒĪ„ÎˇÎŊ Ī„ÎĩÎēÎŧÎˇĪÎ¯Ī‰ĪƒÎˇ.", "link_to_oauth": "ÎŖĪÎŊδÎĩĪƒÎˇ ĪƒĪ„ÎŋÎŊ OAuth", "linked_oauth_account": "Ο OAuth ÎģÎŋÎŗÎąĪÎšÎąĪƒÎŧĪŒĪ‚ ĪƒĪ…ÎŊδέθΡÎēÎĩ", "list": "Î›Î¯ĪƒĪ„Îą", @@ -2213,6 +2217,7 @@ "tag": "Î•Ī„ÎšÎēÎ­Ī„Îą", "tag_assets": "Î•Ī„ÎšÎēÎĩĪ„ÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ", "tag_created": "ΔηÎŧΚÎŋĪ…ĪÎŗÎŽÎ¸ÎˇÎēÎĩ ÎĩĪ„ÎšÎēÎ­Ī„Îą: {tag}", + "tag_face": "Î•Ī€ÎšĪƒÎŽÎŧÎąÎŊĪƒÎˇ ΀΁ÎŋĪƒĪŽĪ€ÎŋĪ…", "tag_feature_description": "ΠÎĩĪÎšÎŽÎŗÎˇĪƒÎˇ ΃Îĩ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēιΚ Î˛Î¯ÎŊĪ„ÎĩÎŋ Ī€ÎŋĪ… ÎĩίÎŊιΚ ÎŋĪÎŗÎąÎŊΉÎŧέÎŊÎą ĪƒĪÎŧΆΉÎŊÎą ÎŧÎĩ ÎģÎŋÎŗÎšÎēÎŦ θέÎŧÎąĪ„Îą ÎĩĪ„ÎšÎēÎĩĪ„ĪŽÎŊ", "tag_not_found_question": "ΔÎĩÎŊ ÎŧĪ€Îŋ΁ÎĩÎ¯Ī„Îĩ ÎŊÎą Î˛ĪÎĩÎ¯Ī„Îĩ ÎŧΚι ÎĩĪ„ÎšÎēÎ­Ī„Îą; ΔηÎŧΚÎŋĪ…ĪÎŗÎŽĪƒĪ„Îĩ ÎŧΚι ÎŊέι ÎĩĪ„ÎšÎēÎ­Ī„Îą.", "tag_people": "Î•Ī€ÎšĪƒÎŽÎŧÎąÎŊĪƒÎˇ ÎąĪ„ĪŒÎŧΉÎŊ", @@ -2394,6 +2399,7 @@ "viewer_remove_from_stack": "ÎšÎąĪ„ÎŦĪÎŗÎˇĪƒÎˇ ÎąĪ€ĪŒ Ī„Îˇ ÎŖĪ„ÎŋÎ¯Î˛Îą", "viewer_stack_use_as_main_asset": "Î§ĪÎŽĪƒÎˇ Ή΂ ÎšĪĪÎšÎŋ ÎŖĪ„ÎŋÎšĪ‡ÎĩίÎŋ", "viewer_unstack": "Î‘Ī€ÎŋĪƒĪ„ÎŋÎ¯Î˛ÎąÎžÎĩ", + "visibility": "ÎŸĪÎąĪ„ĪŒĪ„ÎˇĪ„Îą", "visibility_changed": "Η ÎŋĪÎąĪ„ĪŒĪ„ÎˇĪ„Îą ÎŦÎģÎģιΞÎĩ ÎŗÎšÎą {count, plural, one {# ÎŦĪ„ÎŋÎŧÎŋ} other {# ÎŦĪ„ÎŋÎŧÎą}}", "visual": "ÎŸĪ€Ī„ÎšÎēΌ", "visual_builder": "ÎŸĪ€Ī„ÎšÎēĪŒĪ‚ δΡÎŧΚÎŋĪ…ĪÎŗĪŒĪ‚", diff --git a/i18n/en.json b/i18n/en.json index 956ed03989..add755c05d 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -267,6 +267,8 @@ "notification_enable_email_notifications": "Enable email notifications", "notification_settings": "Notification Settings", "notification_settings_description": "Manage notification settings, including email", + "oauth_allow_insecure_requests": "Allow insecure requests", + "oauth_allow_insecure_requests_description": "WARNING: This disables TLS certificate validation for OAuth requests and may expose you to MITM attacks.", "oauth_auto_launch": "Auto launch", "oauth_auto_launch_description": "Start the OAuth login flow automatically upon navigating to the login page", "oauth_auto_register": "Auto register", @@ -274,9 +276,11 @@ "oauth_button_text": "Button text", "oauth_client_secret_description": "Required for confidential client, or if PKCE (Proof Key for Code Exchange) is not supported for public client.", "oauth_enable_description": "Login with OAuth", + "oauth_end_session_url_description": "Redirect the user to this URI when they log out.", "oauth_mobile_redirect_uri": "Mobile redirect URI", "oauth_mobile_redirect_uri_override": "Mobile redirect URI override", "oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''", + "oauth_prompt_description": "Prompt parameter (e.g. select_account, login, consent)", "oauth_role_claim": "Role Claim", "oauth_role_claim_description": "Automatically grant admin access based on the presence of this claim. The claim may have either 'user' or 'admin'.", "oauth_settings": "OAuth", @@ -441,7 +445,7 @@ "user_successfully_removed": "User {email} has been successfully removed.", "users_page_description": "Admin users page", "version_check_enabled_description": "Enable version check", - "version_check_implications": "The version check feature relies on periodic communication with github.com", + "version_check_implications": "The version check feature relies on periodic communication with {server}", "version_check_settings": "Version Check", "version_check_settings_description": "Enable/disable the new version notification", "video_conversion_job": "Transcode videos", @@ -849,9 +853,12 @@ "create_link_to_share": "Create link to share", "create_link_to_share_description": "Let anyone with the link see the selected photo(s)", "create_new": "CREATE NEW", + "create_new_face": "Create new face", "create_new_person": "Create new person", "create_new_person_hint": "Assign selected assets to a new person", "create_new_user": "Create new user", + "create_person": "Create person", + "create_person_subtitle": "Add a name to the selected face to create and tag the new person", "create_shared_album_page_share_add_assets": "ADD ASSETS", "create_shared_album_page_share_select_photos": "Select Photos", "create_shared_link": "Create shared link", @@ -866,6 +873,7 @@ "crop_aspect_ratio_fixed": "Fixed", "crop_aspect_ratio_free": "Free", "crop_aspect_ratio_original": "Original", + "crop_aspect_ratio_square": "Square", "curated_object_page_title": "Things", "current_device": "Current device", "current_pin_code": "Current PIN code", @@ -880,7 +888,7 @@ "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "Dark", - "dark_theme": "Toggle dark theme", + "dark_theme": "Switch to dark theme", "date": "Date", "date_after": "Date after", "date_and_time": "Date and Time", @@ -891,10 +899,8 @@ "day": "Day", "days": "Days", "deduplicate_all": "Deduplicate All", - "deduplication_criteria_1": "Image size in bytes", - "deduplication_criteria_2": "Count of EXIF data", - "deduplication_info": "Deduplication Info", - "deduplication_info_description": "To automatically preselect assets and remove duplicates in bulk, we look at:", + "default_locale": "Default Locale", + "default_locale_description": "Format dates and numbers based on your browser locale", "delete": "Delete", "delete_action_confirmation_message": "Are you sure you want to delete this asset? This action will move the asset to the server's trash and will prompt if you want to delete it locally", "delete_action_prompt": "{count} deleted", @@ -970,7 +976,7 @@ "downloading_media": "Downloading media", "drop_files_to_upload": "Drop files anywhere to upload", "duplicates": "Duplicates", - "duplicates_description": "Resolve each group by indicating which, if any, are duplicates", + "duplicates_description": "Resolve each group by indicating which, if any, are duplicates.", "duration": "Duration", "edit": "Edit", "edit_album": "Edit album", @@ -1387,9 +1393,12 @@ "library_page_sort_title": "Album title", "licenses": "Licenses", "light": "Light", + "light_theme": "Switch to light theme", "like": "Like", "like_deleted": "Like deleted", + "link": "Link", "link_motion_video": "Link motion video", + "link_to_docs": "For more information, refer to the documentation.", "link_to_oauth": "Link to OAuth", "linked_oauth_account": "Linked OAuth account", "list": "List", @@ -1558,6 +1567,8 @@ "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "mute_memories": "Mute Memories", "my_albums": "My albums", + "my_immich_description": "Copy current page as a My Immich link", + "my_immich_title": "My Immich link", "name": "Name", "name_or_nickname": "Name or nickname", "name_required": "Name is required", @@ -1922,6 +1933,8 @@ "scan_settings": "Scan Settings", "scanning": "Scanning", "scanning_for_album": "Scanning for album...", + "screencast_mode_description": "Show keyboard and mouse event indicators on the screen", + "screencast_mode_title": "Toggle screencast mode", "search": "Search", "search_albums": "Search albums", "search_by_context": "Search by context", @@ -2210,9 +2223,12 @@ "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", + "system_theme": "System theme", + "system_theme_command_description": "Use the system theme ({value})", "tag": "Tag", "tag_assets": "Tag assets", "tag_created": "Created tag: {tag}", + "tag_face": "Tag face", "tag_feature_description": "Browsing photos and videos grouped by logical tag topics", "tag_not_found_question": "Cannot find a tag? Create a new tag.", "tag_people": "Tag People", @@ -2394,6 +2410,7 @@ "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack", + "visibility": "Visibility", "visibility_changed": "Visibility changed for {count, plural, one {# person} other {# people}}", "visual": "Visual", "visual_builder": "Visual builder", diff --git a/i18n/eo.json b/i18n/eo.json index 8e89e960c4..5bc956b284 100644 --- a/i18n/eo.json +++ b/i18n/eo.json @@ -59,12 +59,12 @@ "backup_database_enable_description": "Ebligi kreon de kopioj de datumbazo", "backup_keep_last_amount": "Nombro de antaÅ­aj kopioj konservendaj", "backup_onboarding_1_description": "fora kopio, ĉu en nubo ĉu en alia fizika loko.", - "backup_onboarding_2_description": "lokaj kopioj ĉe diversaj aparatoj, inkluzive ĉefajn dosierojn kaj lokan sekurkopion de tiuj dosieroj.", + "backup_onboarding_2_description": "lokaj kopioj ĉe diversaj aparatoj, inkluzive ĉefajn dosierojn kaj lokan savkopion de tiuj dosieroj.", "backup_onboarding_3_description": "suma nombro de kopioj de viaj datumoj, inkluzive la originajn dosierojn, t.e. 1 fora kopio kaj 2 lokaj kopioj.", - "backup_onboarding_description": "Ni rekomendas strategion de 3-2-1 por protekti viajn datumojn. Vi devus havi sekurkopiojn kaj de viaj fotoj/videoj kaj de la datumbazo de Immich por esti plene sekura.", - "backup_onboarding_footer": "Por pli da informoj pri sekurkopioj kun Immich, bonvolu legi la dokumentaron.", + "backup_onboarding_description": "Ni rekomendas strategion de 3-2-1 por protekti viajn datumojn. Vi devus havi savkopiojn kaj de viaj fotoj/videoj kaj de la datumbazo de Immich por esti plene sekura.", + "backup_onboarding_footer": "Por pli da informoj pri savkopioj kun Immich, bonvolu legi la dokumentaron.", "backup_onboarding_parts_title": "Sekur-kopioj laÅ­ strategio 3-2-1 inkluzivas:", - "backup_onboarding_title": "Sekurkopioj", + "backup_onboarding_title": "Savkopioj", "backup_settings": "AgordaÄĩoj de kopiado de datumbazo", "backup_settings_description": "Administri agordojn pri datumbazo-nekropsio.", "cleared_jobs": "Taskoj forigitaj por: {job}", @@ -192,19 +192,19 @@ "machine_learning_url_description": "La URL-o de la maŝin-lerna servilo. Se vi donas pli ol unu URL-o, la sistemo provos ĉiun servilon unu post la alia ĝis kiam unu sukcese respondas, de la unua ĝis la lasta. Serviloj, kiuj ne respondas, estos dumtempe ignoritaj.", "maintenance_delete_backup": "Forigi savkopion", "maintenance_delete_backup_description": "La dosiero estos por ĉiam forigita.", - "maintenance_delete_error": "Malsukcesis forigi sekurkopion.", + "maintenance_delete_error": "Malsukcesis forigi savkopion.", "maintenance_restore_backup": "RestaÅ­ri savkopion", - "maintenance_restore_backup_description": "Immich estos forigita kaj reinstalita de la elektita sekurkopio. Nova sekurkopio estos kreita antaÅ­e.", - "maintenance_restore_backup_different_version": "Tiu ĉi sekurkopio estis kreita per alia versio de Immich!", - "maintenance_restore_backup_unknown_version": "Ne eblis ektrovi version de la sekurkopio.", - "maintenance_restore_database_backup": "RestaÅ­ri datumbazon el sekurkopio", - "maintenance_restore_database_backup_description": "Reveni al antaÅ­a stato de datumbazo pere de sekurkopio", + "maintenance_restore_backup_description": "Immich estos forigita kaj reinstalita de la elektita savkopio. Nova savkopio estos kreita antaÅ­e.", + "maintenance_restore_backup_different_version": "Tiu ĉi savkopio estis kreita per alia versio de Immich!", + "maintenance_restore_backup_unknown_version": "Ne eblis ektrovi version de la savkopio.", + "maintenance_restore_database_backup": "RestaÅ­ri datumbazon el savkopio", + "maintenance_restore_database_backup_description": "Reveni al antaÅ­a stato de datumbazo pere de savkopio", "maintenance_settings": "Funkcitenado", "maintenance_settings_description": "Ŝalti la funkcitenadan reĝimon de Immich.", "maintenance_start": "Ŝanĝi al funkci-tenada reĝimo", "maintenance_start_error": "Malsukcesis ŝalti funkci-tenadan reĝimon.", - "maintenance_upload_backup": "Alŝuti dosieron de sekurkopio de datumbazo", - "maintenance_upload_backup_error": "Malsukcesis alŝuti sekurkopion, ĉu ĝi havas formaton .sql aÅ­ .sql.gz?", + "maintenance_upload_backup": "Alŝuti dosieron de savkopio de datumbazo", + "maintenance_upload_backup_error": "Malsukcesis alŝuti savkopion, ĉu ĝi havas formaton .sql aÅ­ .sql.gz?", "manage_concurrency": "Administri samtempajn taskojn", "manage_concurrency_description": "Vizitu la paĝon Taskoj por agordi la nombron de samtempaj taskoj", "manage_log_settings": "Administri agordojn pri protokolado", @@ -259,14 +259,14 @@ "notification_email_secure": "SMTPS", "notification_email_secure_description": "Uzi SMTPS (SMTP pere de TLS)", "notification_email_sent_test_email_button": "Sendi testmesaĝon kaj konservi", - "notification_email_setting_description": "Agordoj pri atentigoj per retmesaĝoj", + "notification_email_setting_description": "Agordoj pri sciigoj per retmesaĝoj", "notification_email_test_email": "Sendi testmesaĝon", "notification_email_test_email_failed": "Malsukcesis sendi testmesaĝon, kontrolu la agordaÄĩojn", "notification_email_test_email_sent": "Testmesaĝo estas sendita al {email}. Bonvolu kontroli ĉu ĝi bone alvenis.", "notification_email_username_description": "Uzantonomo por uzi kun la retmesaĝa servilo", - "notification_enable_email_notifications": "Ŝalti retmesaĝajn atentigilojn", - "notification_settings": "Agordoj pri atentigiloj", - "notification_settings_description": "Administri agordojn pri atentigiloj, inkluzive tiujn per retmesaĝoj", + "notification_enable_email_notifications": "Ŝalti sciigojn per retmesaĝo", + "notification_settings": "Agordoj pri sciigoj", + "notification_settings_description": "Administri agordojn pri sciigoj, inkluzive tiujn per retmesaĝoj", "oauth_auto_launch": "Startigi aÅ­tomate", "oauth_auto_launch_description": "AÅ­tomate startigi la OAuth-procezon tuj ĉe la ensaluta paĝo", "oauth_auto_register": "Registri aÅ­tomate", @@ -348,8 +348,8 @@ "template_email_settings": "Ŝablonoj de retmesaĝoj", "template_email_update_album": "Ŝablono por retmesaĝo por ĝisdatigi albumon", "template_email_welcome": "Ŝablono de bonvena retmesaĝo", - "template_settings": "Ŝablonoj de atentigiloj", - "template_settings_description": "Administri tajloritajn skemojn por atentigiloj", + "template_settings": "Ŝablonoj de sciigoj", + "template_settings_description": "Administri tajloritajn skemojn por sciigoj", "theme_custom_css_settings": "Tajlorita CSS", "theme_custom_css_settings_description": "Vi povas ŝanĝi la vidan aspekton de Immich per CSS.", "theme_settings": "Agordoj de la etoso", @@ -441,9 +441,9 @@ "user_successfully_removed": "La uzanto {email} estas forigita.", "users_page_description": "Paĝo por administri uzantojn", "version_check_enabled_description": "Ebligi kontrolon de versio", - "version_check_implications": "La funkcio de kontrolado de versio bezonas de temp' al tempan komunikadon kun github.com", + "version_check_implications": "La funkcio de kontrolado de versio bezonas de temp' al tempan komunikadon kun {server}", "version_check_settings": "Kontrolo de versio", - "version_check_settings_description": "Ŝalti/malŝalti atentigilon pri novaj versioj", + "version_check_settings_description": "Ŝalti/malŝalti sciigojn pri novaj versioj", "video_conversion_job": "Transkodado de videoj", "video_conversion_job_description": "Transkodi videojn por pli vasta kongruo kun retumiloj kaj aparatoj" }, @@ -451,8 +451,8 @@ "admin_password": "Pasvorto de administranto", "administration": "Administrado", "advanced": "Altnivelaj agordoj", - "advanced_settings_clear_image_cache": "Malplenigi kaŝmemoron de bildoj", - "advanced_settings_clear_image_cache_error": "Malsukcesis malplenigi kaŝmemoron", + "advanced_settings_clear_image_cache": "Forviŝi kaŝmemoron de bildoj", + "advanced_settings_clear_image_cache_error": "Malsukcesis forviŝi kaŝmemoron", "advanced_settings_clear_image_cache_success": "Sukcesis liberigi {size}", "advanced_settings_enable_alternate_media_filter_subtitle": "Uzu tiun ĉi agordon por filtri elementojn dum sinkronigo laÅ­ alternativaj kriterioj. Uzu tion ĉi nur se vi vidas, ke la apo ne sukcesas trovi ĉiujn albumojn.", "advanced_settings_enable_alternate_media_filter_title": "[TESTATA] Uzi alternativan filtrilon por sinkronigi albumojn", @@ -527,7 +527,7 @@ "alt_text_qr_code": "Bildo de QR-kodo", "always_keep": "Ĉiam konservi", "always_keep_photos_hint": "La funkcio 'Liberigi spacon' konservos ĉiujn fotojn en tiu ĉi aparato.", - "always_keep_videos_hint": "La funkcio 'Liberigi spacon\" konservos ĉiujn videojn en tiu ĉi aparato.", + "always_keep_videos_hint": "La funkcio 'Liberigi spacon' konservos ĉiujn videojn en tiu ĉi aparato.", "anti_clockwise": "KontraÅ­-horloĝdirekte", "api_key": "API-ŝlosilo", "api_key_description": "Tio ĉi montriĝos nur unufoje. Certiĝu, ke vi kopiis ĝin antaÅ­ ol fermi la fenestron.", @@ -547,7 +547,7 @@ "archive_action_prompt": "{count} aldonita(j) al arÄĨivo", "archive_or_unarchive_photo": "EnarÄĨivigi aÅ­ elarÄĨivigi foton", "archive_page_no_archived_assets": "Neniuj elementoj trovitaj en arÄĨivo", - "archive_page_title": "ArÄĨivo ({count})", + "archive_page_title": "ArÄĨivigi ({count})", "archive_size": "Grandeco de arÄĨivo", "archive_size_description": "Agordu la grandecon de arÄĨivaj dosieroj por elŝuti (en GiB)", "archived": "EnarÄĨivigita(j)", @@ -615,11 +615,11 @@ "autoplay_slideshow": "AÅ­tomate vidigi bildserion", "back": "MalantaÅ­en", "back_close_deselect": "MalantaÅ­en, fermi, aÅ­ malelekti", - "background_backup_running_error": "Sekurkopiado jam estas fone okazanta, do ne eblas nun lanĉi alian sekurkopiadon", + "background_backup_running_error": "Savkopiado jam estas fone okazanta, do ne eblas nun lanĉi alian savkopiadon", "background_location_permission": "Rajtigo fone uzi geografian lokon", "background_location_permission_content": "Por ŝanĝi retaliron dum fona funkciado, Immich devas *ĉiam* havi atingorajton al lokiga informo, por povi legi nomojn de vifiaj retoj", "background_options": "Agordoj pri fonaj funkcioj", - "backup": "Sekurkopio", + "backup": "Savkopio", "backup_album_selection_page_albums_device": "Albumoj en la aparato ({count})", "backup_album_selection_page_albums_tap": "Tuŝeti por inkluzivi, duoble tuŝeti por ekskludi", "backup_album_selection_page_assets_scatter": "Foje elementoj troviĝas disÄĩetitaj al pluraj albumoj, do albumoj povas esti inkluzivitaj aÅ­ ekskluzivitaj de la savkopiado.", @@ -675,36 +675,476 @@ "backup_controller_page_total_sub": "Ĉiuj unikaj fotoj kaj videoj el elektitaj albumoj", "backup_controller_page_turn_off": "Malŝalti malfonan savkopiadon", "backup_controller_page_turn_on": "Ŝalti malfonan savkopiadon", + "backup_controller_page_uploading_file_info": "Alŝutiĝas informoj pri dosiero", + "backup_err_only_album": "Ne eblas forigi la solan albumon", + "backup_error_sync_failed": "Sinkronigo malsukcesis.", + "backup_info_card_assets": "elementoj", + "backup_manual_cancelled": "Nuligita", + "backup_manual_in_progress": "Alŝuto jam progresas. Provu poste", + "backup_manual_success": "Sukceso", + "backup_manual_title": "Statuso de alŝuto", + "backup_options": "Agordoj pri savkopioj", + "backup_options_page_title": "Agordoj pri savkopioj", "backup_setting_subtitle": "Administri agordojn pri fona kaj malfona alŝutado", "backup_settings_subtitle": "Administri agordojn pri alŝutado", + "backup_upload_details_page_more_details": "Tuŝu ĉi tie por pli da detaloj", + "backward": "MalantaÅ­en", + "biometric_auth_enabled": "Biometria ensaluto ŝaltita", + "biometric_locked_out": "Via biometria ensalutkapablo estas blokita", + "biometric_no_options": "Neniuj biometriaj ebloj estas disponeblaj", + "biometric_not_available": "Tiu ĉi aparato ne havas funkcion por biometria ensaluto", + "birthdate_saved": "Naskiĝdato ŝukcese konservita", + "birthdate_set_description": "La naskiĝdato estas uzita por kalkuli la aĝon de la homo je la momento de iu foto.", + "blurred_background": "Malklarigita fono", + "bugs_and_feature_requests": "Cimoj kaj petoj por novaj funkcioj", + "build": "Versio", + "build_image": "Bildo de la versio", + "bulk_delete_duplicates_confirmation": "Ĉu vi certas, ke vi volas amase forigi {count, plural, one {# duoblaÄĩon} other {# duoblaÄĩojn}}? Tiel, vi konservos la plej grandan elementon el ĉiu grupo kaj porĉiame forigos duoblaÄĩojn. Ne eblas malfari tion!", + "bulk_keep_duplicates_confirmation": "Ĉu vi certas, ke vi volas konservi {count, plural, one {# duoblaÄĩon} other {# duoblaÄĩojn}}? Tio solvos ĉiujn duoblajn grupojn sen forigi ion ajn.", + "bulk_trash_duplicates_confirmation": "Ĉu vi certas, ke vi volas amase forigi {count, plural, one {# duoblaÄĩon} other {# duoblaÄĩojn}}? Tiel, vi konservos la plej grandan elementon el ĉiu grupo kaj porĉiame forigos duoblaÄĩojn.", + "buy": "Aĉeti Immich", + "cache_settings_clear_cache_button": "Forviŝi kaŝmemoron", + "cache_settings_clear_cache_button_title": "Forviŝas la kaŝmemoron de la apo. Tio malrapidigos la apon ĝis kiam ĝi finos rekonstrui la kaŝon.", + "cache_settings_duplicated_assets_clear_button": "FORVIŜI", + "cache_settings_duplicated_assets_subtitle": "Fotoj kaj videoj ignoritaj de la apo", + "cache_settings_duplicated_assets_title": "DuoblaÄĩoj ({count})", + "cache_settings_statistics_album": "Bildetoj de la biblioteko", + "cache_settings_statistics_full": "Plenaj bildoj", + "cache_settings_statistics_shared": "Bildetoj de dividitaj albumoj", + "cache_settings_statistics_thumbnail": "Bildetoj", + "cache_settings_statistics_title": "Uzo de kaŝmemoro", + "cache_settings_subtitle": "Regas la uzadon de kaŝmemoro fare de la apo", + "cache_settings_tile_subtitle": "Regas konduton pri loka stokado", + "cache_settings_tile_title": "Loka stokado", + "cache_settings_title": "Agordoj pri kaŝmemoro", + "camera": "Fotilo", + "camera_brand": "Fabrikanto de fotilo", + "camera_model": "Modelo de fotilo", + "cancel": "Nuligi", + "cancel_search": "Nuligi serĉon", + "canceled": "Nuligita", + "canceling": "Nuligado", + "cannot_merge_people": "Ne eblas kunfandi tiujn homojn", + "cannot_undo_this_action": "Ne eblas malfari tion!", + "cannot_update_the_description": "Ne eblas ĝisdatigi la priskribon", + "cast": "Elsendi", + "cast_description": "Agordi disponeblajn celojn por elsendoj", + "change_date": "Ŝanĝi daton", + "change_description": "Ŝanĝi priskribon", + "change_display_order": "Ŝanĝi vicordon de vidigo", + "change_expiration_time": "Ŝanĝi horon de eksvalidiĝo", + "change_location": "Ŝanĝi lokon", + "change_name": "Ŝanĝi nomon", + "change_name_successfully": "Nomo sukcese ŝanĝita", + "change_password": "Ŝanĝi pasvorton", + "change_password_description": "AÅ­ tio ĉi estas via unua ensaluto, aÅ­ la sistemo ricevis peton ŝanĝigi vian pasvorton. Bonvolu tajpi novan pasvorton ĉi-sube.", + "change_password_form_confirm_password": "Konfirmu pasvorton", + "change_password_form_description": "Saluton {name},\n\nAÅ­ tio ĉi estas via unua ensaluto, aÅ­ la sistemo ricevis peton ŝanĝigi vian pasvorton. Bonvolu tajpi novan pasvorton ĉi-sube.", + "change_password_form_log_out": "Elsalutu ĉe ĉiuj aliaj aparatoj", + "change_password_form_log_out_description": "Oni rekomendas elsaluti ĉe ĉiuj aliaj aparatoj", + "change_password_form_new_password": "Nova pasvorto", + "change_password_form_password_mismatch": "Pasvortoj ne kongruas", + "change_password_form_reenter_new_password": "Re-tajpu novan pasvorton", + "change_pin_code": "Ŝanĝi PIN-kodon", + "change_trigger": "Ŝanĝi ekagilon", + "change_trigger_prompt": "Ĉu vi certas, ke vi volas ŝanĝi la ekagilon? Tio forigos ĉiujn ekzistantajn agojn kaj filtrilojn.", + "change_your_password": "Ŝanĝi vian pasvorton", + "changed_visibility_successfully": "Sukcese ŝanĝis videblecon", + "charging": "Ŝargado", + "charging_requirement_mobile_backup": "Por fona savkopiado, vi devas konekti la aparaton al ŝargilo", + "check_corrupt_asset_backup": "Kontroli por koruptitaj savkopioj de elementoj", + "check_corrupt_asset_backup_button": "Kontroli", + "check_corrupt_asset_backup_description": "Fari tiun ĉi kontrolon nur per vifio kaj post kiam ĉiuj elementoj havas savkopion. La kontrolo povas daÅ­ri kelkajn minutojn.", + "check_logs": "Kontroli protokolojn", + "checksum": "Kontrolsumo", + "choose_matching_people_to_merge": "Elekti duobligitajn homojn por kunfandi", + "city": "Urbo", + "cleanup_confirm_description": "Immich trovis savkopion en la servilo de {count} elementoj (kreitajn antaÅ­ {date}). Ĉu vi volas forigi la kopiojn de el tiu ĉi aparato?", + "cleanup_confirm_prompt_title": "Forigi el tiu ĉi aparato?", + "cleanup_deleted_assets": "Movis {count} elementojn al la rubujo de la aparato", + "cleanup_deleting": "Movado al rubujo...", + "cleanup_found_assets": "Trovis {count} elementojn kun savkopio", + "cleanup_found_assets_with_size": "Trovis {count} elementojn kun savkopio ({size})", "cleanup_icloud_shared_albums_excluded": "Dividitaj albumoj ĉe iCloud estas ekskluditaj de la analizado", - "cleanup_step3_description": "Serĉi fotojn kaj videojn kun sekurkopio ĉe la servilo, laÅ­ la elektita limdato kaj filtriloj.", + "cleanup_no_assets_found": "Neniuj elementoj trovitaj per la ĉi-supraj kriterioj. La funkcio 'Liberigi spacon' forigas nur elementojn, kiuj havas savkopion ĉe la servilo", + "cleanup_preview_title": "Forigotaj elementoj ({count})", + "cleanup_step3_description": "Serĉi fotojn kaj videojn kun savkopio ĉe la servilo, laÅ­ la elektita limdato kaj filtriloj.", + "cleanup_step4_summary": "{count} elementoj (kreitaj antaÅ­ {date}) forigotaj de via aparato. Fotoj restos disponeblaj (pere de la servilo) en la apo Immich.", + "cleanup_trash_hint": "Por povi reuzi la liberigitan spacon, malfermu la 'galeria' apo de via aparato kaj malplenigu la rubujon", + "clear": "Forviŝi", + "clear_all": "Forviŝi ĉiujn kampojn", + "clear_all_recent_searches": "Forviŝi ĉiujn lastatempajn serĉojn", + "clear_file_cache": "Forviŝi dosier-kaŝon", + "clear_message": "Forviŝi mesaĝon", + "clear_value": "Forviŝi valoron", + "client_cert_dialog_msg_confirm": "Bone", + "client_cert_enter_password": "Tajpu pasvorton", + "client_cert_import": "Importi", + "client_cert_import_success_msg": "Atestilo sukcese importita", + "client_cert_invalid_msg": "Nevalida atestilo-dosiero, aÅ­ malĝusta pasvorto", + "client_cert_password_message": "Tajpu la pasvorton por tiu ĉi atestilo", + "client_cert_password_title": "Pasvorto de atestilo", + "client_cert_remove_msg": "Klient-atestilo forigita", + "client_cert_subtitle": "Nur la formato PKCS12 (.p12, .pfx) estas akceptita. Eblas importi/forigi atestilon nur antaÅ­ ol ensaluti", + "client_cert_title": "Klient-atestilo SSL [EKSPERIMENTA]", + "clockwise": "Horloĝdirekte", + "close": "Fermi", + "collapse": "Maletendi", + "collapse_all": "Maletendi ĉiujn", + "color": "Koloro", + "color_theme": "Kolor-temo", + "command": "Komando", + "command_palette_prompt": "Rapide trovi paĝojn, agojn aÅ­ komandojn", + "command_palette_to_close": "por fermi", + "command_palette_to_navigate": "por eniri", + "command_palette_to_select": "por elekti", + "command_palette_to_show_all": "por ĉion montri", + "comment_deleted": "Komento forigita", + "comment_options": "Agoj pri komento", + "comments_and_likes": "Komentoj kaj ŝatoj", + "comments_are_disabled": "Komentoj estas malebligitaj", + "common_create_new_album": "Krei novan albumon", + "completed": "Finfarita", + "confirm": "Konfirmi", + "confirm_admin_password": "Konfirmi administran pasvorton", + "confirm_delete_face": "Ĉu vi certas ke vi volas forigi la vizaĝon de {name} de tiu elemento?", + "confirm_delete_shared_link": "Ĉu vi certas, ke vi volas forigi tiun ligilon?", + "confirm_keep_this_delete_others": "Ĉiuj elementoj en la stako krom tiu ĉi estos forigitaj. Ĉu vi certas, ke vi volas tion?", + "confirm_new_pin_code": "Konfirmi novan PIN-kodon", + "confirm_password": "Konfirmi pasvorton", + "confirm_tag_face": "Ĉu vi volas etikedi tiun ĉi vizaĝon kiel {name}?", + "confirm_tag_face_unnamed": "Ĉu vi volas etikedi tiun ĉi vizaĝon?", + "connected_device": "Konektita aparato", + "connected_to": "Konektita al", + "contain": "Alĝustigi", + "context": "Kunteksto", + "continue": "DaÅ­rigi", + "control_bottom_app_bar_create_new_album": "Krei novan albumon", + "control_bottom_app_bar_delete_from_immich": "Forigi el Immich", + "control_bottom_app_bar_delete_from_local": "Forigi el aparato", + "control_bottom_app_bar_edit_location": "Redakti lokon", + "control_bottom_app_bar_edit_time": "Redakti daton kaj horon", + "control_bottom_app_bar_share_link": "Dividi ligilon", + "control_bottom_app_bar_share_to": "Dividi al", + "control_bottom_app_bar_trash_from_immich": "Movi al rubujo", + "copied_image_to_clipboard": "Bildo kopiita al tondujo.", + "copied_to_clipboard": "Kopiita al tondujo!", + "copy_error": "Kopii eraron", + "copy_file_path": "Kopii dosiervojon", + "copy_image": "Kopii bildon", + "copy_link": "Kopii ligilon", + "copy_link_to_clipboard": "Kopii ligilon al tondujo", + "copy_password": "Kopii pasvorton", + "copy_to_clipboard": "Kopii al tondujo", + "country": "Lando", + "cover": "Kovri", + "covers": "Kovriloj", + "create": "Krei", + "create_album": "Krei albumon", + "create_album_page_untitled": "Sen titolo", + "create_api_key": "Krei API-ŝlosilon", + "create_first_workflow": "Krei unuan laborfluon", + "create_library": "Krei bibliotekon", + "create_link": "Krei ligilon", + "create_link_to_share": "Krei ligilon por dividi", + "create_link_to_share_description": "Permesi, ke iu ajn kun la ligilo povu vidi la elektita(j)n foto(j)n", + "create_new": "KREI NOVAN", + "create_new_face": "Krei novan vizaĝon", + "create_new_person": "Krei novan homon", + "create_new_person_hint": "Atribui elektitajn elementojn al nova homo", + "create_new_user": "Krei novan uzanton", + "create_person": "Krei homon", + "create_person_subtitle": "Aldoni nomon al la elektita vizaĝo por krei kaj etikedi novan homon", + "create_shared_album_page_share_add_assets": "ALDONI ELEMENTOJN", + "create_shared_album_page_share_select_photos": "Elekti fotojn", + "create_shared_link": "Krei dividitan ligilon", + "create_tag": "Krei etikedon", + "create_tag_description": "Krei novan etikedon. Por ingitaj etikedoj, bonvolu tajpi la plenan vojon de la etikedo, inkluzive suprenstrekoj (\"/\").", + "create_user": "Krei uzanton", + "create_workflow": "Krei laborfluon", + "created": "Kreita", + "created_at": "Kreita", + "creating_linked_albums": "Kreado de ligitaj albumoj...", + "crop": "Stuci", + "crop_aspect_ratio_fixed": "Fiksita", + "crop_aspect_ratio_free": "Libera", + "crop_aspect_ratio_original": "Originala", + "crop_aspect_ratio_square": "Kvadrata", + "curated_object_page_title": "Objektoj", + "current_device": "Aktuala aparato", + "current_pin_code": "Aktuala PIN-kodo", + "current_server_address": "Aktuala adreso de servilo", + "custom_date": "Elekti propran daton", + "custom_locale": "Propra lokaÄĩaro", + "custom_locale_description": "Prezenti datojn, horojn kaj numerojn laÅ­ la elektita lingvo kaj regiono", + "custom_url": "Propra URL", + "cutoff_date_description": "Konservi fotojn el la lastajâ€Ļ", + "cutoff_day": "{count, plural, one {tago} other {tagoj}}", + "cutoff_year": "{count, plural, one {jaro} other {jaroj}}", + "daily_title_text_date": "E, dd MMM", + "daily_title_text_date_year": "E, dd MMM, yyyy", + "dark": "Malhela", + "dark_theme": "Ŝanĝi al hela reĝimo", + "date": "Dato", + "date_after": "Dato post", + "date_and_time": "Dato kaj horo", + "date_before": "Dato antaÅ­", + "date_format": "E, LLL d, y â€ĸ h:mm a", + "date_of_birth_saved": "Naskiĝdato sukcese registrita", + "date_range": "Dato-intervalo", + "day": "Tago", + "days": "Tagoj", + "deduplicate_all": "SenduoblaÄĩigi ĉion", + "default_locale": "DefaÅ­lta lokaÄĩaro", + "default_locale_description": "Prezenti datojn kaj numerojn laÅ­ la lokaÄĩaro de via retumilo", + "delete": "Forigi", + "delete_action_confirmation_message": "Ĉu vi certas, ke vi volas forigi tiun ĉi elementon? Tiu ago movos ĝin al la rubujo ĉe la servilo, kaj demandos ĉu vi volas forigi ĝin de via aparato", + "delete_action_prompt": "{count} forigita(j)", + "delete_album": "Forigi albumon", + "delete_api_key_prompt": "Ĉu vi certas, ke vi volas forigi tiu ĉi API-ŝlosilon?", + "delete_dialog_alert": "Tiuj elementoj estos porĉiame forigitaj de Immich kaj de via aparato", + "delete_dialog_alert_local": "Tiuj ĉi elementoj estos forigitaj de via aparato, sed restos disponeblaj ĉe la servilo de Immich", + "delete_dialog_alert_local_non_backed_up": "Kelkaj el tiuj elementoj ne havas savkopion ĉe Immich kaj estos porĉiame forigitaj de via aparato", + "delete_dialog_alert_remote": "Tiuj elementoj estos porĉiame forigitaj de la Immich-servilo", + "delete_dialog_ok_force": "Forigi ĉiuokaze", + "delete_dialog_title": "Forigi por ĉiam", + "delete_duplicates_confirmation": "Ĉu vi certas, ke vi volas porĉiame forigi tiujn ĉi duoblaÄĩojn?", + "delete_face": "Forigi vizaĝon", + "delete_key": "Forigi ŝlosilon", + "delete_library": "Forigi bibliotekon", + "delete_link": "Forigi ligilon", + "delete_local_action_prompt": "{count} loke forigita(j)", + "delete_local_dialog_ok_backed_up_only": "Forigi nur elementojn, kiuj havas savkopiojn", + "delete_local_dialog_ok_force": "Forigi ĉiuokaze", + "delete_others": "Forigi la aliajn", + "delete_permanently": "Forigi por ĉiam", + "delete_permanently_action_prompt": "{count} forigita(j) por ĉiam", + "delete_shared_link": "Forigi dividitan ligilon", + "delete_shared_link_dialog_title": "Forigi dividitan ligilon", + "delete_tag": "Forigi etikedon", + "delete_tag_confirmation_prompt": "Ĉu vi certas, ke vi volas forigi la etikedon {tagName}?", + "delete_user": "Forigi uzanton", + "deleted_shared_link": "Dividita ligilo nun forigita", + "deletes_missing_assets": "Forigas elementojn, kiuj mankas ĉe la disko", + "description": "Priskribo", + "description_input_hint_text": "Aldoni priskribon...", + "description_input_submit_error": "Eraro okazis dum ĝisdatigo de priskribo. Kontrolu protokolon por pli da detaloj", + "deselect_all": "Malelekti ĉion", + "details": "Detaloj", + "direction": "Direkto", + "disable": "Malebligi", + "disabled": "Malebligita", + "disallow_edits": "Malpermesi redaktojn", + "discord": "Discord", + "discover": "Malkovri", + "discovered_devices": "Malkovritaj aparatoj", + "dismiss_all_errors": "Ignori ĉiujn erarojn", + "dismiss_error": "Ignori eraron", + "display_options": "Vidigi tiajn elementojn", + "display_order": "Vicordo de vidigo", + "display_original_photos": "Montri originalajn fotojn", + "display_original_photos_setting_description": "Prefere montri originalan foton anstataÅ­ bildeton se la originalo havas retumil-kongruan formaton. Tio povas malrapidigi vidigon de elementoj.", + "do_not_show_again": "Ne plu montri tiun ĉi mesaĝon", + "documentation": "Dokumentaro", + "done": "Finite", + "download": "Elŝuti", + "download_action_prompt": "Elŝutado de {count} elementoj", + "download_canceled": "Elŝuto nuligita", + "download_complete": "Elŝuto finita", + "download_enqueue": "Elŝuto en atendovico", + "download_error": "Eraro de elŝuto", + "download_failed": "Elŝuto malsukcesis", + "download_finished": "Elŝuto finiĝis", + "download_include_embedded_motion_videos": "Enkorpigitaj videoj", + "download_include_embedded_motion_videos_description": "Inkluzivi videon, enkorpigitan en mov-fotoj, kiel apartan dosieron", + "download_notfound": "Elŝuto ne trovita", + "download_original": "Elŝuti originalon", + "download_paused": "Elŝuto paÅ­zita", + "download_settings": "Elŝutado", "download_settings_description": "Administri agordojn pri elŝutado de elementoj", + "download_started": "Elŝuto komenciĝis", + "download_sucess": "Elŝuto sukcesis", + "download_sucess_android": "La elemento estas elŝutita al DCIM/Immich", + "download_waiting_to_retry": "BaldaÅ­ reprovos elŝuton", + "downloading": "Elŝutado", + "downloading_asset_filename": "Elŝutado de elemento {filename}", + "downloading_from_icloud": "Elŝutado el iCloud", + "downloading_media": "Elŝutado de elementoj", + "drop_files_to_upload": "Demetu dosierojn ĉi tien por alŝuti", + "duplicates": "DuoblaÄĩoj", + "duplicates_description": "Solvu ĉiun grupon indikante tiujn, kiuj estas eventualaj duoblaÄĩoj.", + "duration": "DaÅ­ro", + "edit": "Redakti", + "edit_album": "Redakti albumon", + "edit_avatar": "Redakti profilbildon", + "edit_birthday": "Redakti naskiĝtagon", + "edit_date": "Redakti daton", + "edit_date_and_time": "Redakti daton kaj horon", + "edit_date_and_time_action_prompt": "{count} datoj kaj horoj redaktitaj", + "edit_date_and_time_by_offset": "Deŝovi daton", + "edit_date_and_time_by_offset_interval": "Nova intervalo: de {from} ĝis {to}", + "edit_description": "Redakti priskribon", + "edit_description_prompt": "Bonvolu elekti novan priskribon:", "edit_exclusion_pattern": "Redakti skemon de ekskludo", + "edit_faces": "Redakti vizaĝojn", + "edit_key": "Redakti ŝlosilon", + "edit_link": "Redakti ligilon", + "edit_location": "Redakti lokon", + "edit_location_action_prompt": "{count} loko(j) redaktita(j)", + "edit_location_dialog_title": "Loko", + "edit_name": "Redakti nomon", + "edit_people": "Redakti homojn", + "edit_tag": "Redakti etikedon", + "edit_title": "Redakti titolon", + "edit_user": "Redakti uzanton", + "edit_workflow": "Redakti laborfluon", + "editor": "Redaktilo", + "editor_close_without_save_prompt": "La ŝanĝoj ne konserviĝos", + "editor_close_without_save_title": "Ĉu fermi redaktilon?", + "editor_confirm_reset_all_changes": "Ĉu vi certas, ke vi volas forÄĩeti ĉiujn ŝanĝojn?", + "editor_discard_edits_confirm": "ForÄĩeti ŝanĝojn", + "editor_discard_edits_prompt": "Vi havas nekonservitajn ŝanĝojn. Ĉu vi certas, ke vi volas forigi ilin?", + "editor_discard_edits_title": "ForÄĩeti ŝanĝojn?", + "editor_edits_applied_error": "Malsukcesis apliki redaktojn", + "editor_edits_applied_success": "Redaktoj sukcese aplikiĝis", + "editor_flip_horizontal": "Inversigi horizontale", + "editor_flip_vertical": "Inversigi vertikale", + "editor_handle_corner": "{corner, select, top_left {Supra-maldekstra} top_right {Supra-dekstra} bottom_left {Suba-maldekstra} bottom_right {Suba-dekstra} other {Ajna}} angula tenilo", + "editor_handle_edge": "{edge, select, top {Supra} bottom {Suba} left {Maldekstra} right {Dekstra} other {Ajna}} randa tenilo", + "editor_orientation": "Orientiĝo", + "editor_reset_all_changes": "Forviŝi ŝanĝojn", + "editor_rotate_left": "Turni 90Âē kontraÅ­-horloĝdirekte", + "editor_rotate_right": "Turni 90Âē horloĝdirekte", + "email": "Retadreso", + "email_notifications": "Sciigoj per retmesaĝo", + "empty_folder": "Tiu ĉi dosierujo estas malplena", + "empty_trash": "Malplenigi rubujon", + "empty_trash_confirmation": "Ĉu vi certas, ke vi volas malplenigi la rubujon? Ĉiuj elementoj en la rubujo estas por ĉiam forigitaj de Immich.\nNe eblas malfari tion!", + "enable": "Ŝalti", + "enable_backup": "Ŝalti savkopiadon", + "enable_biometric_auth_description": "Tajpu vian PIN-kodon por ŝalti biometrian ensalutadon", + "enabled": "Ŝaltita", + "end_date": "Fina dato", + "enqueued": "En atendovico", + "enter_wifi_name": "Tajpu nomon de vifio", + "enter_your_pin_code": "Tajpu vian PIN-kodon", + "enter_your_pin_code_subtitle": "Tajpu vian PIN-kodon por atingi la ŝlositan dosierujon", + "error": "Eraro", + "error_change_sort_album": "Malsukcesis ŝanĝi vicordon de album-elementoj", + "error_delete_face": "Eraro dum forigo de vizaĝo el elemento", + "error_getting_places": "Eraro dum serĉo de lokoj", + "error_loading_albums": "Eraro dum ŝargado de albumoj", + "error_loading_image": "Eraro dum ŝargado de bildo", + "error_loading_partners": "Eraro dum ŝargado de partneroj: {error}", + "error_retrieving_asset_information": "Eraro dum ŝargado de informoj pri elemento", + "error_saving_image": "Eraro: {error}", + "error_tag_face_bounding_box": "Eraro dum etikedado de vizaĝo - ne eblis trovi koordinatojn de kadro", + "error_title": "Eraro - io misis", + "error_while_navigating": "Eraro dum navigado al elemento", "errors": { + "cannot_navigate_next_asset": "Ne eblis navigi al sekva elemento", + "cannot_navigate_previous_asset": "Ne eblas navigi al antaÅ­a elemento", + "cant_apply_changes": "Ne eblas apliki ŝanĝojn", + "cant_change_activity": "Ne eblas {enabled, select, true {malŝalti} other {ŝalti}} tiun agon", + "cant_change_asset_favorite": "Ne eblas ŝanĝi preferon por tiu elemento", + "cant_change_metadata_assets_count": "Ne eblas ŝanĝi metadatumojn de {count, plural, one {# elemento} other {# elementoj}}", + "cant_get_faces": "Ne eblas trovi vizaĝojn", + "cant_get_number_of_comments": "Ne eblas trovi nombron da komentoj", + "cant_search_people": "Ne eblas serĉi homojn", + "cant_search_places": "Ne eblas serĉi lokojn", + "error_adding_assets_to_album": "Eraro dum ŝargado de elementoj al albumo", + "error_adding_users_to_album": "Eraro dum aldono de uzantoj al albumo", + "error_deleting_shared_user": "Eraro dum forigo de dividita uzanto", + "error_downloading": "Eraro dum elŝuto de {filename}", + "error_hiding_buy_button": "Eraro dum kaŝado de butono 'aĉeti'", + "error_removing_assets_from_album": "Eraro dum forigo de elementoj el albumo; kontrolu konzolon por detaloj", + "error_selecting_all_assets": "Eraro dum elekto de ĉiuj elementoj", "exclusion_pattern_already_exists": "Tiu ĉi skemo de ekskludo jam ekzistas.", + "failed_to_create_album": "Malsukcesis krei albumon", + "failed_to_create_shared_link": "Malsukcesis krei dividitan ligilon", + "failed_to_edit_shared_link": "Malsukcesis redakti dividitan ligilon", + "failed_to_get_people": "Malsukcesis trovi homojn", + "failed_to_keep_this_delete_others": "Malsukcesis konservi tiun ĉi elementon kaj forigi la aliajn", + "failed_to_load_asset": "Malsukcesis ŝargi elementon", + "failed_to_load_assets": "Malsukcesis ŝargi elementojn", + "failed_to_load_notifications": "Malsukcesis ŝargi sciigojn", + "failed_to_load_people": "Malsukcesis ŝargi homojn", + "failed_to_remove_product_key": "Malsukcesis forigi var-ŝalosilon", + "failed_to_reset_pin_code": "Malsukcesis restarigi PIN-kodon", + "failed_to_stack_assets": "Malsukcesis staki elementojn", + "failed_to_unstack_assets": "Malsukcesis malstaki elementojn", + "failed_to_update_notification_status": "Malsukcesis ĝisdatigi statuson de sciigoj", + "incorrect_email_or_password": "Neĝusta retadreso aÅ­ pasvorto", + "library_folder_already_exists": "Tiu ĉi import-vojo jam ekzistas.", + "page_not_found": "Paĝo ne trovita", + "paths_validation_failed": "Evidentiĝis, ke {paths, plural, one {# vojo estas nevalida} other {# vojoj estas nevalidaj}}", + "profile_picture_transparent_pixels": "Ne eblas havi travideblaj bilderoj en profilbildo. Bonvolu zomi kaj/aÅ­ ŝovi la bildon al loko sen tiaj bilderoj.", + "quota_higher_than_disk_size": "Vi donis kvoton pli grandan ol la disko mem", + "something_went_wrong": "Io misis", + "unable_to_add_album_users": "Ne eblas aldoni uzantojn al la albumo", + "unable_to_add_assets_to_shared_link": "Ne eblas aldoni elementojn al la dividita ligilo", + "unable_to_add_comment": "Ne eblas aldoni komenton", "unable_to_add_exclusion_pattern": "Ne eblas aldoni skemon de ekskludo", + "unable_to_add_partners": "Ne eblas aldoni partnerojn", + "unable_to_add_remove_archive": "Ne eblas {archived, select, true {forigi elementon de} other {aldoni elementon al}} la arÄĨivo", + "unable_to_add_remove_favorites": "Ne eblas {favorite, select, true {aldoni elementon al} other {forigi elementon de}} preferataÄĩoj", + "unable_to_change_favorite": "Ne eblas ŝanĝi preferon por tiu elemento", + "unable_to_create": "Ne eblis krei laborfluon", "unable_to_delete_exclusion_pattern": "Ne eblas forigi skemon de ekskludo", + "unable_to_delete_workflow": "Ne eblis forigi laborfluon", "unable_to_edit_exclusion_pattern": "Ne eblas redakti skemon de ekskludo", "unable_to_scan_libraries": "Ne eblas analizi biblitekojn", - "unable_to_scan_library": "Ne eblas analizi biblitekon" + "unable_to_scan_library": "Ne eblas analizi biblitekon", + "unable_to_update_workflow": "Ne eblis ĝisdatigi laborfluon" }, "exclusion_pattern": "Skemo de ekskludo", + "expand": "Etendi", + "expand_all": "Etendi ĉiujn", "explore": "Esplori", "explorer": "Foliumilo", + "favorite": "PreferataÄĩo", + "favorite_action_prompt": "{count} aldonita(j) al PreferataÄĩoj", + "favorite_or_unfavorite_photo": "Aldoni/forigi foton al/de preferataÄĩoj", + "favorites": "PreferataÄĩoj", + "favorites_page_no_favorites": "Neniuj preferataj elementoj trovitaj", + "free_up_space": "Liberigi spacon", + "free_up_space_description": "Vi forigos fotojn kaj/aÅ­ videojn, kiuj havas savkopiojn en la servilo, por liberigi spacon en via aparato. La kopioj en la servilo restos.", "general": "Ĝeneralaj", + "home_page_favorite_err_local": "AnkoraÅ­ ne eblas aldoni lokajn elementojn al PreferataÄĩoj; ignorita(j)", + "home_page_favorite_err_partner": "AnkoraÅ­ ne eblas aldoni elementojn de partnero al PreferataÄĩoj; ignorita(j)", + "keep_favorites": "Konservi preferataÄĩojn", "manage_media_access_settings": "Malfermi agordaÄĩaron", "manage_the_app_settings": "Agordi la apon", + "map_settings_only_show_favorites": "Montri nur preferataÄĩojn", "missing": "Netraktitaj", "networking_subtitle": "Administri agordojn pri finpunktoj de la servilo", "no_devices": "Neniuj aprobitaj aparatoj", "no_explore_results_message": "Alŝutu pli da fotoj por esplori vian kolekton.", + "no_favorites_message": "Aldoni al PreferataÄĩoj por rapide retrovi viajn plej bonajn bildojn kaj videojn", + "no_notifications": "Neniuj sciigoj", "no_results_description": "Provu sinonimon aÅ­ pli ĝeneralan ŝlosilvorton", + "notification_permission_dialog_content": "Por ŝalti sciigojn, iru al Agordoj kaj elektu 'permesi'.", + "notification_permission_list_tile_content": "Donu permeson por ŝalti sciigojn.", + "notification_permission_list_tile_enable_button": "Ŝalti sciigojn", + "notification_permission_list_tile_title": "Permeso pri sciigoj", + "notification_toggle_setting_description": "Ŝalti sciigojn per retmesaĝo", + "notifications": "Sciigoj", + "notifications_setting_description": "Administri sciigojn", + "only_favorites": "Nur preferataÄĩoj", "preferences_settings_subtitle": "Administri agordojn pri la apo", "purchase_settings_server_activated": "La administranto respondecas pri la ŝlosilo de aÅ­tentikeco por la servilo", + "rating_clear": "Forviŝi pritakson", "refresh": "Denove", + "remove_from_favorites": "Forigi el preferataÄĩoj", + "removed_from_favorites": "Forigita(j) el preferataÄĩoj", + "removed_from_favorites_count": "{count, plural, other {Forigis #}} el PreferataÄĩoj", "rescan": "Reanalizi", "reset": "Restartigi", + "reset_sqlite_clear_app_data": "Forviŝi datumojn", + "reset_sqlite_confirmation": "Ĉu vi certas, ke vi volas forviŝi la datumojn de la apo? Tio forigos ĉiujn agordojn kaj elsalutigos vin.", + "reset_sqlite_confirmation_note": "Noto: vi devos relanĉi la apon por la forviŝo.", + "reset_sqlite_done": "Datumoj de la apo estas forviŝitaj. Bonvolu relanĉi Immich kaj ensalutu denove.", + "scaffold_body_error_unrecoverable": "Neriparebla eraro okazis. Bonvolu sendi al ni la eraron kaj la stakspuron per Discord aÅ­ per Github por ke ni povu helpi. Vi povas forviŝi la ĉi-subajn datumojn de la apo se vi volas.", "scan": "Analizi", "scan_all_libraries": "Analizi ĉiujn bibliotekojn", "scan_library": "Analizi", @@ -712,12 +1152,34 @@ "scanning": "Analizado", "scanning_for_album": "Serĉado de albumo...", "search_suggestion_list_smart_search_hint_1": "Inteligenta serĉado defaÅ­lte estas ŝaltita. Por serĉi metadatumojn, uzu sintakson tiel ", + "setting_notifications_subtitle": "Redakti viajn preferojn pri sciigoj", + "start_date": "Komenca dato", + "start_date_before_end_date": "Komenca dato devas esti antaÅ­ fina dato", + "to_favorite": "Aldoni al preferataÄĩoj", + "trigger_description": "Evento, kiu ekfunkciigas la laborfluon", + "unfavorite": "Forigi el preferataÄĩoj", + "unfavorite_action_prompt": "{count} forigita(j) el PreferataÄĩoj", + "untitled_workflow": "Sentitola laborfluo", "upload_concurrency": "Nombro da samtempaj alŝutoj", "user_pin_code_settings_description": "Administri vian PIN-kodon", "user_purchase_settings_description": "Administri vian aĉeton", "view_links": "Vidi ligilojn", "week": "Semajno", "wifi_name": "Nomo de Vifireto", + "workflow_delete_prompt": "Ĉu vi certas, ke vi volas forigi tiun ĉi laborfluon?", + "workflow_deleted": "Laborfluo forigita", + "workflow_description": "Priskribo de laborfluo", + "workflow_info": "Informoj pri laborfluo", + "workflow_json": "JSON de laborfluo", + "workflow_json_help": "Redakti la agordojn pri la laborfluo per formato JSON. La ŝanĝoj sinkroniĝos al la vidiga konstruilo.", + "workflow_name": "Nomo de laborfluo", + "workflow_navigation_prompt": "Ĉu vi certas, ke vi volas foriri sen konservi viajn ŝanĝojn?", + "workflow_summary": "Resumo de laborfluo", + "workflow_update_success": "Laborfluo sukcese ĝisdatigita", + "workflow_updated": "Laborfluo ĝisdatigita", + "workflows": "Laborfluoj", + "workflows_help_text": "Laborfluo aÅ­tomatigas agojn pri elementoj, laÅ­ ekigiloj kaj filtriloj", "year": "Jaro", - "yes": "Jes" + "yes": "Jes", + "zero_to_clear_rating": "tuŝu 0 por forviŝi la pritakson de la elemento" } diff --git a/i18n/es.json b/i18n/es.json index fe82e3a093..722c8fd98c 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -17,13 +17,13 @@ "add_a_name": "AÃąadir un nombre", "add_a_title": "AÃąadir título", "add_action": "AÃąadir acciÃŗn", - "add_action_description": "Haga clic para aÃąadir una acciÃŗn a realizar", + "add_action_description": "Haz clic para aÃąadir una acciÃŗn a realizar", "add_assets": "AÃąadir recursos", "add_birthday": "AÃąadir un cumpleaÃąos", "add_endpoint": "AÃąadir punto final", "add_exclusion_pattern": "AÃąadir patrÃŗn de exclusiÃŗn", "add_filter": "AÃąadir filtro", - "add_filter_description": "Haga clic para aÃąadir una condiciÃŗn de filtro", + "add_filter_description": "Haz clic para aÃąadir una condiciÃŗn de filtro", "add_location": "AÃąadir ubicaciÃŗn", "add_more_users": "AÃąadir mÃĄs usuarios", "add_partner": "AÃąadir miembro", @@ -372,7 +372,7 @@ "transcoding_audio_codec": "Codec de audio", "transcoding_audio_codec_description": "Opus es la opciÃŗn de mayor calidad, pero tiene menor compatibilidad con dispositivos o software antiguos.", "transcoding_bitrate_description": "Vídeos con una tasa de bits superior a la mÃĄxima o que no estÃĄn en un formato aceptado", - "transcoding_codecs_learn_more": "Para obtener mÃĄs informaciÃŗn sobre la terminología utilizada aquí, consulte la documentaciÃŗn de FFmpeg sobre el cÃŗdec H.264, el cÃŗdec HEVC y el cÃŗdec VP9.", + "transcoding_codecs_learn_more": "Para obtener mÃĄs informaciÃŗn sobre la terminología utilizada aquí, consulta la documentaciÃŗn de FFmpeg sobre el cÃŗdec H.264, el cÃŗdec HEVC y el cÃŗdec VP9.", "transcoding_constant_quality_mode": "Modo de calidad constante", "transcoding_constant_quality_mode_description": "ICQ es mejor que CQP, pero algunos dispositivos de aceleraciÃŗn de hardware no admiten este modo. Al configurar esta opciÃŗn, se preferirÃĄ el modo especificado cuando se utilice codificaciÃŗn basada en calidad. NVENC lo ignora porque no es compatible con ICQ.", "transcoding_constant_rate_factor": "Factor de tasa constante (-crf)", @@ -441,7 +441,7 @@ "user_successfully_removed": "El usuario {email} ha sido eliminado con Êxito.", "users_page_description": "PÃĄgina de usuarios administradores", "version_check_enabled_description": "Activar la comprobaciÃŗn de la versiÃŗn", - "version_check_implications": "La funciÃŗn de comprobaciÃŗn de versiones depende de la comunicaciÃŗn periÃŗdica con github.com", + "version_check_implications": "La funciÃŗn de comprobaciÃŗn de versiones depende de la comunicaciÃŗn periÃŗdica con {server}", "version_check_settings": "Verificar versiÃŗn", "version_check_settings_description": "Activar/desactivar la notificaciÃŗn de nueva versiÃŗn", "video_conversion_job": "Transcodificar vídeos", @@ -849,9 +849,12 @@ "create_link_to_share": "Crear enlace compartido", "create_link_to_share_description": "Permitir que cualquier persona con el enlace vea la(s) foto(s) seleccionada(s)", "create_new": "CREAR NUEVO", + "create_new_face": "Crear nueva cara", "create_new_person": "Crear nueva persona", "create_new_person_hint": "Asignar los recursos seleccionados a una nueva persona", "create_new_user": "Crear nuevo usuario", + "create_person": "Crear persona", + "create_person_subtitle": "AÃąade un nombre a la cara seleccionada para crear y etiquetar a la nueva persona", "create_shared_album_page_share_add_assets": "AÑADIR RECURSOS", "create_shared_album_page_share_select_photos": "Seleccionar fotos", "create_shared_link": "Crear un enlace compartido", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "Fijado", "crop_aspect_ratio_free": "Libre", "crop_aspect_ratio_original": "Original", + "crop_aspect_ratio_square": "Cuadrado", "curated_object_page_title": "Objetos", "current_device": "Dispositivo actual", "current_pin_code": "PIN actual", @@ -880,7 +884,7 @@ "daily_title_text_date": "E dd, MMM", "daily_title_text_date_year": "E dd de MMM, yyyy", "dark": "Oscuro", - "dark_theme": "Alternar tema oscuro", + "dark_theme": "Cambiar a tema oscuro", "date": "Fecha", "date_after": "Fecha posterior", "date_and_time": "Fecha y hora", @@ -891,10 +895,8 @@ "day": "Día", "days": "Días", "deduplicate_all": "Deduplicar todo", - "deduplication_criteria_1": "TamaÃąo de imagen en bytes", - "deduplication_criteria_2": "Conteo de datos EXIF", - "deduplication_info": "InformaciÃŗn de DeduplicaciÃŗn", - "deduplication_info_description": "Para automÃĄticamente preseleccionar recursos y eliminar duplicados en conjunto, nosotros consideramos lo siguiente:", + "default_locale": "ConfiguraciÃŗn regional predeterminada", + "default_locale_description": "Formatear fechas y nÃēmeros segÃēn la configuraciÃŗn regional del navegador", "delete": "Eliminar", "delete_action_confirmation_message": "ÂŋEstÃĄ seguro que desea eliminar este recurso? Esta acciÃŗn lo moverÃĄ a la papelera del servidor y le preguntarÃĄ si desea eliminarlo localmente", "delete_action_prompt": "{count} eliminados", @@ -970,7 +972,7 @@ "downloading_media": "Descargando medios", "drop_files_to_upload": "Suelta los archivos en cualquier lugar para subirlos", "duplicates": "Duplicados", - "duplicates_description": "Resuelva cada grupo indicando, en cada caso, cuales estÃĄn duplicados", + "duplicates_description": "Resuelve cada grupo indicando cuÃĄles son duplicados, si los hay.", "duration": "DuraciÃŗn", "edit": "Editar", "edit_album": "Editar ÃĄlbum", @@ -1023,7 +1025,7 @@ "enable_biometric_auth_description": "Introduce tu cÃŗdigo PIN para habilitar la autentificaciÃŗn biomÊtrica", "enabled": "Habilitado", "end_date": "Fecha final", - "enqueued": "Agregado a la cola", + "enqueued": "AÃąadido a la cola", "enter_wifi_name": "Introduce el nombre Wi-Fi", "enter_your_pin_code": "Introduce tu cÃŗdigo PIN", "enter_your_pin_code_subtitle": "Introduce tu cÃŗdigo PIN para acceder a la carpeta protegida", @@ -1086,7 +1088,7 @@ "unable_to_add_partners": "No se pueden aÃąadir miembros", "unable_to_add_remove_archive": "No se pudo {archived, select, true {eliminar el recurso del} other {aÃąadir el recurso al}} archivo", "unable_to_add_remove_favorites": "No se pudo {favorite, select, true {aÃąadir el recuso a} other {eliminar el recurso de}} los favoritos", - "unable_to_archive_unarchive": "No se pudo {archived, select, true {agregar el elemento al} other {quitar el elemento del}} archivo", + "unable_to_archive_unarchive": "No se pudo {archived, select, true {aÃąadir el elemento al} other {quitar el elemento del}} archivo", "unable_to_change_album_user_role": "No se puede cambiar la funciÃŗn del usuario del ÃĄlbum", "unable_to_change_date": "No se puede cambiar la fecha", "unable_to_change_description": "Imposible cambiar la descripciÃŗn", @@ -1165,7 +1167,7 @@ }, "errors_text": "Errores", "exclusion_pattern": "PatrÃŗn de exclusiÃŗn", - "exif": "EXIF", + "exif": "Exif", "exif_bottom_sheet_description": "AÃąadir descripciÃŗnâ€Ļ", "exif_bottom_sheet_description_error": "Error al actualizar la descripciÃŗn", "exif_bottom_sheet_details": "DETALLES", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "Título del ÃĄlbum", "licenses": "Licencias", "light": "Claro", + "light_theme": "Cambiar a tema claro", "like": "Me gusta", "like_deleted": "Me gusta eliminado", "link_motion_video": "Enlazar vídeo en movimiento", + "link_to_docs": "Para mÃĄs informaciÃŗn, consulta la documentaciÃŗn.", "link_to_oauth": "Enlace a OAuth", "linked_oauth_account": "Cuenta OAuth vinculada", "list": "Lista", @@ -2213,6 +2217,7 @@ "tag": "Etiqueta", "tag_assets": "Etiquetar recursos", "tag_created": "Etiqueta creada: {tag}", + "tag_face": "Etiquetar cara", "tag_feature_description": "Explore fotos y videos agrupados por temas de etiquetas lÃŗgicas", "tag_not_found_question": "ÂŋNo encuentra una etiqueta? Crea una nueva etiqueta.", "tag_people": "Etiquetar personas", @@ -2394,6 +2399,7 @@ "viewer_remove_from_stack": "Quitar de la pila", "viewer_stack_use_as_main_asset": "Usar como recurso principal", "viewer_unstack": "Desapilar", + "visibility": "Visibilidad", "visibility_changed": "Visibilidad cambiada para {count, plural, one {# persona} other {# personas}}", "visual": "Visual", "visual_builder": "Constructor visual", diff --git a/i18n/et.json b/i18n/et.json index f2726add15..201b341d15 100644 --- a/i18n/et.json +++ b/i18n/et.json @@ -441,7 +441,7 @@ "user_successfully_removed": "Kasutaja {email} edukalt eemaldatud.", "users_page_description": "Kasutajate haldamise leht", "version_check_enabled_description": "Luba versioonikontroll", - "version_check_implications": "Versioonikontroll vajab perioodilist Ãŧhendumist github.com-iga", + "version_check_implications": "Versioonikontroll vajab perioodilist Ãŧhendumist {server}-iga", "version_check_settings": "Versioonikontroll", "version_check_settings_description": "Luba/keela uue versiooni teavitus", "video_conversion_job": "Videote transkodeerimine", @@ -849,9 +849,12 @@ "create_link_to_share": "Lisa jagamiseks link", "create_link_to_share_description": "Luba kÃĩigil, kellel on link, valitud pilte näha", "create_new": "LISA UUS", + "create_new_face": "Lisa uus nägu", "create_new_person": "Lisa uus isik", "create_new_person_hint": "Seosta valitud Ãŧksused uue isikuga", "create_new_user": "Lisa uus kasutaja", + "create_person": "Lisa isik", + "create_person_subtitle": "Lisa valitud näole nimi, et uus isik lisada ja sildistada", "create_shared_album_page_share_add_assets": "LISA ÜKSUSEID", "create_shared_album_page_share_select_photos": "Vali fotod", "create_shared_link": "Loo jagatud link", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "Fikseeritud", "crop_aspect_ratio_free": "Vaba", "crop_aspect_ratio_original": "Originaalne", + "crop_aspect_ratio_square": "Ruut", "curated_object_page_title": "Asjad", "current_device": "Praegune seade", "current_pin_code": "Praegune PIN-kood", @@ -880,7 +884,7 @@ "daily_title_text_date": "d. MMMM", "daily_title_text_date_year": "d. MMMM yyyy", "dark": "Tume", - "dark_theme": "LÃŧlita tume teema", + "dark_theme": "Vali tume teema", "date": "Kuupäev", "date_after": "Kuupäev pärast", "date_and_time": "Kuupäev ja kellaaeg", @@ -891,10 +895,8 @@ "day": "Päev", "days": "Päeva", "deduplicate_all": "Dedubleeri kÃĩik", - "deduplication_criteria_1": "Pildi suurus baitides", - "deduplication_criteria_2": "EXIF andmete hulk", - "deduplication_info": "Dedubleerimise info", - "deduplication_info_description": "Üksuste automaatsel eelvalimisel ja duplikaatide eemaldamisel vÃĩetakse arvesse:", + "default_locale": "Vaikimisi lokaat", + "default_locale_description": "Vorminda kuupäevad ja arvud vastavalt brauseri lokaadile", "delete": "Kustuta", "delete_action_confirmation_message": "Kas oled kindel, et soovid selle Ãŧksuse kustutada? See toiming liigutab Ãŧksuse serveri prÃŧgikasti ja kÃŧsib, kas soovid selle lokaalselt kustutada", "delete_action_prompt": "{count} kustutatud", @@ -970,7 +972,7 @@ "downloading_media": "Üksuste allalaadimine", "drop_files_to_upload": "Failide Ãŧleslaadimiseks sikuta need ÃŧkskÃĩik kuhu", "duplicates": "Duplikaadid", - "duplicates_description": "Lahenda iga grupp, valides duplikaadid, kui neid on", + "duplicates_description": "Lahenda iga grupp, valides duplikaadid, kui neid on.", "duration": "Kestus", "edit": "Muuda", "edit_album": "Muuda albumit", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "Albumi pealkiri", "licenses": "Litsentsid", "light": "Hele", + "light_theme": "Vali hele teema", "like": "Meeldib", "like_deleted": "Meeldimine kustutatud", "link_motion_video": "Lingi liikuv video", + "link_to_docs": "Rohkema info saamiseks vaata dokumentatsiooni.", "link_to_oauth": "Ühenda OAuth", "linked_oauth_account": "OAuth konto Ãŧhendatud", "list": "Loend", @@ -2213,6 +2217,7 @@ "tag": "Silt", "tag_assets": "Sildista Ãŧksuseid", "tag_created": "Lisatud silt: {tag}", + "tag_face": "Sildista nägu", "tag_feature_description": "Fotode ja videote lehitsemine siltide kaupa grupeeritult", "tag_not_found_question": "Ei leia silti? Lisa uus silt.", "tag_people": "Sildista inimesi", @@ -2394,6 +2399,7 @@ "viewer_remove_from_stack": "Eemalda virnast", "viewer_stack_use_as_main_asset": "Kasuta peamise Ãŧksusena", "viewer_unstack": "Eralda", + "visibility": "Nähtavus", "visibility_changed": "{count, plural, one {# isiku} other {# isiku}} nähtavus muudetud", "visual": "Visuaalne", "visual_builder": "Visuaalne koostaja", diff --git a/i18n/eu.json b/i18n/eu.json index 04443a14f8..2ac0bc6e32 100644 --- a/i18n/eu.json +++ b/i18n/eu.json @@ -5,8 +5,10 @@ "acknowledge": "Onartu", "action": "Ekintza", "action_common_update": "Eguneratu", + "action_description": "Ekintza multzoa iragazitako aktiboetan aplikatzeko", "actions": "Ekintzak", "active": "Aktibo", + "active_count": "Aktibo: {count}", "activity": "Jarduera", "activity_changed": "Jarduera {enabled, select, true {ezarrita dago} other {ez dago ezarrita}}", "add": "Gehitu", @@ -20,6 +22,8 @@ "add_birthday": "Urtebetetzea gehitu", "add_endpoint": "Endpoint-a gehitu", "add_exclusion_pattern": "Bazterketa eredua gehitu", + "add_filter": "Gehitu iragazkia", + "add_filter_description": "Klik egin iragazki baldintza bat gehitzeko", "add_location": "Kokapena gehitu", "add_more_users": "Erabiltzaile gehiago gehitu", "add_partner": "Kidea gehitu", @@ -30,41 +34,78 @@ "add_to_album": "Albumera gehitu", "add_to_album_bottom_sheet_added": "{album} -(e)ra gehitu", "add_to_album_bottom_sheet_already_exists": "Dagoeneko {album} albumenean", + "add_to_album_bottom_sheet_some_local_assets": "Aktibo lokal batzuk ezin izan dira albumera gehitu", + "add_to_album_toggle": "Txandakatu aukeraketa {album}-arentzat", "add_to_albums": "Albumetara gehitu", "add_to_albums_count": "Albumetara gehitu ({count})", + "add_to_bottom_bar": "Gehitu hona", "add_to_shared_album": "Gehitu partekatutako albumera", + "add_upload_to_stack": "Gehitu karga pilara", "add_url": "URL-a gehitu", + "add_workflow_step": "Gehitu fluxu pausoa", "added_to_archive": "Artxibategira gehituta", - "added_to_favorites": "Faboritoetara gehituta", - "added_to_favorites_count": "{count, number} faboritoetara gehituta", + "added_to_favorites": "Gogokoetara gehituta", + "added_to_favorites_count": "{count, number} gogokoetara gehituta", "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", + "asset_offline_description": "Kanpo-liburutegiko aktibo hau es da diskoan aurkitu eta zaborrontzira mugitu da. Fitxategia liburutegian bertan mugitu bada, bilatu denbora lerroan dagokion aktibo berria. Aktiboa berreskuratzeko, mesedez ziurtatu fitxategiaren helbidea Immich-ek eskuratu dezakela eta eskaneatu liburutegia.", "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_database": "Sortu datubasearen dump-a", + "backup_database_enable_description": "Gaitu datu base dump-ak", + "backup_keep_last_amount": "Mantendu beharreko dump kopurua", + "backup_onboarding_1_description": "kanpo kopia hodeiean edo beste kokaleku fisiko batean.", + "backup_onboarding_2_description": "kopia lokalak gailu ezberdinetan. Honek fitxategi nagusiak eta fitxategi horien babeskopia lokalak barneratzen ditu.", + "backup_onboarding_3_description": "datuen kopiak guztira, fitxategi originalak barne. Honek kanpo kopia 1 eta 2 kopia lokal barne ditu.", + "backup_onboarding_description": "3-2-1 babeskopia estrategia gomendatzen da zure datuak babesteko. Babeskopia soluzio osoa lortzeko, kargatutako irudien/bideoen kopiak gorde beharko zenituzke. Immich datu-basearena baita ere.", "backup_onboarding_footer": "Immich-en babes kopiei buruzko informazio gehiago nahi baduzu, mesedez irakurri dokumentazioa.", + "backup_onboarding_parts_title": "3-2-1 babes-kopia batek barne hartzen du:", "backup_onboarding_title": "Babes Kopiak", + "backup_settings": "Datu-base Dump-aren Ezarpenak", + "backup_settings_description": "Datu-base dump-aren ezarpenak kudeatu.", + "cleared_jobs": "Garbitutako lanak honentzak: {job}", + "config_set_by_file": "Konfigurazioa konfigurazio-fitxategi baten bidez dago ezarria", "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?", + "copy_config_to_clipboard_description": "Kopiatu momentuko sistema-konfigurazioa JSON objetu formatuan arbelean", "create_job": "Gehitu zeregina", + "cron_expression": "Cron adierazpena", + "cron_expression_description": "Ezarri eskaneatzeko tartea cron formatua erabiliz. Informazio gehiago lortzeko, jo mesedez Crontab Guru adibidera", + "cron_expression_presets": "Cron adierazpenaren aurrezarpenak", "disable_login": "Desgaitu saio hastea", + "duplicate_detection_job_description": "Exekutatu ikasketa automatikoa aktiboetan antzeko irudiak detektatzeko. Bilaketa Adimendunean oinarritzen da", + "export_config_as_json_description": "Deskargatu momentuko sistema konfigurazioa JSON fitxategi moduan", + "external_libraries_page_description": "Administratzailearen kanpo liburutegi orrialdea", "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_prefer_embedded_preview": "Nahiago aurrebista txertatua", + "image_prefer_wide_gamut": "Nahiago gamut zabala", "image_preview_title": "Aurreikusiaen Konfigurazioa", + "image_progressive": "Progresiboa", "image_quality": "Kalitatea", "image_resolution": "Erresoluzioa", "image_settings": "Argazkien Konfigurazioa", + "image_settings_description": "Kudeatu sortutako irudien kalitatea eta erresoluzioa", "image_thumbnail_title": "Argazki Txikien Konfigurazioa", + "import_config_from_json_description": "Inportatu sistema konfigurazioa JSON konfigurazio fitxategia kargatuz", + "job_concurrency": "{job} konkurrentzia", "job_created": "Zeregina sortuta", "job_settings": "Zereginaren konfigurazioa", + "job_settings_description": "Kudeatu lanen konkurrentzia", + "jobs_over_time": "Lanak denboran zehar", + "library_created": "Sortutako liburutegia: {library}", + "library_deleted": "Liburutegia ezabatuta", + "library_details": "Liburutegiaren xehetasunak", + "library_remove_folder_prompt": "Ziur zaude inportazio karpeta hau ezabatu nahi duzula?", "logging_enable_description": "Gaitu erregistroak", "logging_level_description": "Erregistroak gaituta daudenean, nolako erregistro maila erabili.", "logging_settings": "Erregistroak", diff --git a/i18n/fa.json b/i18n/fa.json index e7d681d92f..0a29a09d83 100644 --- a/i18n/fa.json +++ b/i18n/fa.json @@ -5,6 +5,7 @@ "acknowledge": "Ų…ØĒ؈ØŦŲ‡ Ø´Ø¯Ų…", "action": "ØšŲ…Ų„ÚŠØąØ¯", "action_common_update": "Ø¨Ų‡â€Œ ØąŲˆØ˛â€ŒØąØŗØ§Ų†ÛŒ", + "action_description": "ØĒؚدادی ØšŲ…Ų„ÛŒØ§ØĒ Ø¨ØąØ§ÛŒ Ø§Ų†ØŦØ§Ų… ØąŲˆÛŒ Ø¯Ø§Ø¯Ų‡â€ŒŲ‡Ø§ÛŒ ŲÛŒŲ„ØĒØą Ø´Ø¯Ų‡", "actions": "ØšŲ…Ų„ÚŠØąØ¯", "active": "ŲØšØ§Ų„", "active_count": "ŲØšØ§Ų„: {count}", @@ -14,8 +15,14 @@ "add_a_location": "Ø§ŲØ˛ŲˆØ¯Ų† یڊ Ų…ÚŠØ§Ų†", "add_a_name": "Ø§ŲØ˛ŲˆØ¯Ų† Ų†Ø§Ų…", "add_a_title": "Ø§ŲØ˛ŲˆØ¯Ų† ØšŲ†ŲˆØ§Ų†", + "add_action": "Ø§ŲØ˛ŲˆØ¯Ų† ØšŲ…Ų„ÛŒØ§ØĒ", + "add_action_description": "Ø¨ØąØ§ÛŒ Ø§ŲØ˛ŲˆØ¯Ų† ؈ Ø§ØšŲ…Ø§Ų„ یڊ ØšŲ…Ų„ÛŒØ§ØĒ ÚŠŲ„ÛŒÚŠ ÚŠŲ†ÛŒØ¯", + "add_assets": "Ø§ŲØ˛ŲˆØ¯Ų† ØšÚŠØŗ یا ŲÛŒŲ„Ų…", "add_birthday": "Ø§ŲØ˛ŲˆØ¯Ų† ØĒØ§ØąÛŒØŽ ØĒŲˆŲ„Ø¯", + "add_endpoint": "Ø§ŲØ˛ŲˆØ¯Ų† ŲžØ§ÛŒØ§Ų†Ų‡", "add_exclusion_pattern": "Ø§ŲØ˛ŲˆØ¯Ų† Ø§Ų„Ú¯ŲˆÛŒ Ø§ØŗØĒØĢŲ†Ø§", + "add_filter": "Ø§ŲØ˛ŲˆØ¯Ų† ŲÛŒŲ„ØĒØą", + "add_filter_description": "Ø¨ØąØ§ÛŒ Ø§ŲØ˛ŲˆØ¯Ų† یڊ Ø´ØąØˇ ŲÛŒŲ„ØĒØą ÚŠŲ„ÛŒÚŠ ÚŠŲ†ÛŒØ¯", "add_location": "Ø§ŲØ˛ŲˆØ¯Ų† Ų…ÚŠØ§Ų†", "add_more_users": "Ø§ŲØ˛ŲˆØ¯Ų† ÚŠØ§ØąØ¨ØąŲ‡Ø§ÛŒ بیشØĒØą", "add_partner": "Ø§ŲØ˛ŲˆØ¯Ų† Ø´ØąÛŒÚŠ", @@ -27,25 +34,38 @@ "add_to_album_bottom_sheet_added": "Ø¨Ų‡ ØĸŲ„Ø¨ŲˆŲ… {album} اØļØ§ŲŲ‡ شد", "add_to_album_bottom_sheet_already_exists": "Ų‚Ø¨Ų„Ø§ Ø¯Øą ØĸŲ„Ø¨ŲˆŲ… {album} Ų…ŲˆØŦŲˆØ¯ Ø§ØŗØĒ", "add_to_album_bottom_sheet_some_local_assets": "Ø¨ØąØŽÛŒ Ø§Ø˛ Ų…Ø­ØĒŲˆØ§Ų‡Ø§ÛŒ Ų…Ø­Ų„ÛŒ ØąØ§ Ų†Ø´Ø¯ Ø¨Ų‡ ØĸŲ„Ø¨ŲˆŲ… اØļØ§ŲŲ‡ ÚŠØąØ¯", + "add_to_album_toggle": "ØĒØēÛŒÛŒØą ؈ØļØšÛŒØĒ Ø§Ų†ØĒ؎اب Ø¨ØąØ§ÛŒ {album}", "add_to_albums": "Ø§ŲØ˛ŲˆØ¯Ų† Ø¨Ų‡ ØĸŲ„Ø¨ŲˆŲ…", "add_to_albums_count": "Ø§ŲØ˛ŲˆØ¯Ų† Ø¨Ų‡ ØĸŲ„Ø¨ŲˆŲ… Ų‡Ø§ {count}", "add_to_bottom_bar": "Ø§ŲØ˛ŲˆØ¯Ų† Ø¨Ų‡", "add_to_shared_album": "Ø§ŲØ˛ŲˆØ¯Ų† Ø¨Ų‡ ØĸŲ„Ø¨ŲˆŲ… اشØĒØąØ§ÚŠÛŒ", "add_upload_to_stack": "Ø§ŲØ˛ŲˆØ¯Ų† ŲØ§ÛŒŲ„ Ø§ØąØŗØ§Ų„ÛŒ Ø¨Ų‡ Ų…ØŦŲ…ŲˆØšŲ‡", "add_url": "Ø§ŲØ˛ŲˆØ¯Ų† ØĸØ¯ØąØŗ URL", + "add_workflow_step": "Ø§ŲØ˛ŲˆØ¯Ų† یڊ Ų…ØąØ­Ų„Ų‡ Ø¨Ų‡ ØąŲˆŲ†Ø¯ ÚŠØ§Øą", "added_to_archive": "Ø¨Ų‡ ØĸØąØ´ÛŒŲˆ اØļØ§ŲŲ‡ شد", "added_to_favorites": "Ø¨Ų‡ ØšŲ„Ø§Ų‚Ų‡ Ų…Ų†Ø¯ÛŒ Ų‡Ø§ اØļØ§ŲŲ‡ شد", "added_to_favorites_count": "{count, number} ØĒا Ø¨Ų‡ ØšŲ„Ø§Ų‚Ų‡ Ų…Ų†Ø¯ÛŒ Ų‡Ø§ اØļØ§ŲŲ‡ شد", "admin": { "add_exclusion_pattern_description": "Ø§Ų„Ú¯ŲˆŲ‡Ø§ÛŒ Ø§ØŗØĒØĢŲ†Ø§ ØąØ§ اØļØ§ŲŲ‡ ÚŠŲ†ÛŒØ¯. ŲžØ´ØĒÛŒØ¨Ø§Ų†ÛŒ Ø§Ø˛ Ú¯Ų„Ø§Ø¨ÛŒŲ†Ú¯ با Ø§ØŗØĒŲØ§Ø¯Ų‡ Ø§Ø˛ *, ** ؈ ? ؈ØŦŲˆØ¯ Ø¯Ø§ØąØ¯. Ø¨ØąØ§ÛŒ Ų†Ø§Ø¯ÛŒØ¯Ų‡ Ú¯ØąŲØĒŲ† ØĒŲ…Ø§Ų… ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ Ø¯Øą Ų‡Øą Ø¯Ø§ÛŒØąÚŠØĒŲˆØąÛŒ با Ų†Ø§Ų… \"Raw\"، Ø§Ø˛ \"**/Raw/**\" Ø§ØŗØĒŲØ§Ø¯Ų‡ ÚŠŲ†ÛŒØ¯. Ø¨ØąØ§ÛŒ Ų†Ø§Ø¯ÛŒØ¯Ų‡ Ú¯ØąŲØĒŲ† ØĒŲ…Ø§Ų… ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ÛŒÛŒ ÚŠŲ‡ با \".tif\" ŲžØ§ÛŒØ§Ų† Ų…ÛŒâ€ŒÛŒØ§Ø¨Ų†Ø¯ØŒ Ø§Ø˛ \"**/*.tif\" Ø§ØŗØĒŲØ§Ø¯Ų‡ ÚŠŲ†ÛŒØ¯. Ø¨ØąØ§ÛŒ Ų†Ø§Ø¯ÛŒØ¯Ų‡ Ú¯ØąŲØĒŲ† یڊ Ų…ØŗÛŒØą Ų…ØˇŲ„Ų‚ØŒ Ø§Ø˛ \"/path/to/ignore/**\" Ø§ØŗØĒŲØ§Ø¯Ų‡ ÚŠŲ†ÛŒØ¯.", "admin_user": "Ø§Ø¯Ų…ÛŒŲ†", + "asset_offline_description": "Ø§ÛŒŲ† ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ Ø¯Ø§Ø¯Ų‡â€ŒÛŒ Ø¨ÛŒØąŲˆŲ†ÛŒ ØąŲˆÛŒ Ų…Ø­Ų„ Ø°ØŽÛŒØąŲ‡â€ŒØŗØ§Ø˛ÛŒ ŲžÛŒØ¯Ø§ Ų†Ø´Ø¯ ؈ Ø¨Ų‡ ØŗØˇŲ„ ØĸØ´ØēŲ„ Ų…Ų†ØĒŲ‚Ų„ شد. Ø§Ú¯Øą ŲØ§ÛŒŲ„ Ų…ŲˆØąØ¯ Ų†Ø¸Øą Ø¯Øą Ø¯Ø§ØŽŲ„ ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ ØŦابØŦØ§Ø¯Ų‡ Ø´Ø¯Ų‡ØŒ ØĒØ§ÛŒŲ…Ų„Ø§ÛŒŲ† ØŽŲˆØ¯ ØąØ§ Ø¨ØąØ§ÛŒ Ø¯Ø§Ø¯Ų‡â€ŒÛŒ ØŦدید چک ÚŠŲ†ÛŒØ¯. Ø¨ØąØ§ÛŒ Ø¨Ø§Ø˛ÛŒØ§Ø¨ÛŒ Ø§ÛŒŲ† Ø¯Ø§Ø¯Ų‡ Ų„ØˇŲØ§ Ų…ØˇŲ…ØĻŲ† Ø´ŲˆÛŒØ¯ ÚŠŲ‡ Ų…ØŗÛŒØą ŲØ§ÛŒŲ„ Ø˛ÛŒØą ØĒŲˆØŗØˇ Immich Ų‚Ø§Ø¨Ų„ Ø¯ØŗØĒØąØŗ Ø§ØŗØĒ ØŗŲžØŗ ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ ØąØ§ Ø§ØŗÚŠŲ† ÚŠŲ†ÛŒØ¯.", "authentication_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ø§Ø­ØąØ§Ø˛ Ų‡ŲˆÛŒØĒ", "authentication_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ ØąŲ…Ø˛ ØšØ¨ŲˆØąØŒ OAuth، ؈ ØŗØ§ÛŒØą ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ø§Ø­ØąØ§Ø˛ Ų‡ŲˆÛŒØĒ", "authentication_settings_disable_all": "Øĸیا Ų…ØˇŲ…ØĻŲ† Ų‡ØŗØĒید ÚŠŲ‡ Ų…ÛŒâ€ŒØŽŲˆØ§Ų‡ÛŒØ¯ ØĒŲ…Ø§Ų… ØąŲˆØ´â€ŒŲ‡Ø§ÛŒ ŲˆØąŲˆØ¯ ØąØ§ ØēÛŒØąŲØšØ§Ų„ ÚŠŲ†ÛŒØ¯ØŸ ŲˆØąŲˆØ¯ Ø¨Ų‡ ØˇŲˆØą ÚŠØ§Ų…Ų„ ØēÛŒØąŲØšØ§Ų„ ØŽŲˆØ§Ų‡Ø¯ شد.", "authentication_settings_reenable": "Ø¨ØąØ§ÛŒ ŲØšØ§Ų„ ØŗØ§Ø˛ÛŒ Ų…ØŦدد Ø§Ø˛ Ø¯ØŗØĒŲˆØą ØŗØąŲˆØą Ø§ØŗØĒŲØ§Ø¯Ų‡ ÚŠŲ†ÛŒØ¯.", "background_task_job": "ŲˆØ¸Ø§ÛŒŲ ŲžØŗâ€ŒØ˛Ų…ÛŒŲ†Ų‡", + "backup_database": "اØļØ§ŲŲ‡ ÚŠØąØ¯Ų† یڊ Ų†ØŗØŽŲ‡ ÚŠŲžÛŒ Ø§Ø˛ دیØĒØ§Ø¨ÛŒØŗ", + "backup_database_enable_description": "ŲØšØ§Ų„ ÚŠØąØ¯Ų† ÚŠŲžÛŒ Ø§Ø˛ دیØĒØ§Ø¨ÛŒØŗ", + "backup_keep_last_amount": "ØĒؚداد ÚŠŲžÛŒâ€ŒŲ‡Ø§ÛŒ Ų‚Ø¨Ų„ÛŒ Ø¨ØąØ§ÛŒ Ų†Ú¯Ų‡ داشØĒŲ†", + "backup_onboarding_1_description": "ÚŠŲžÛŒ ØŽØ§ØąØŦی ØąŲˆÛŒ ؁Øļای Ø§Ø¨ØąÛŒ یا یڊ Ų…Ø­Ų„ ŲÛŒØ˛ÛŒÚŠÛŒ Ø¯ÛŒÚ¯Øą.", + "backup_onboarding_2_description": "ÚŠŲžÛŒâ€ŒŲ‡Ø§ÛŒ Ų…Ø­Ų„ÛŒ ØąŲˆÛŒ Ø¯ØŗØĒÚ¯Ø§Ų‡â€ŒŲ‡Ø§ÛŒ Ø¯ÛŒÚ¯Øą. Ø§ÛŒŲ† Ø´Ø§Ų…Ų„ ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ÛŒ اØĩŲ„ÛŒ ؈ ŲžØ´ØĒÛŒØ¨Ø§Ų†â€ŒŲ‡Ø§ÛŒ Ų…Ø­Ų„ÛŒ Ø§Ø˛ ØĸŲ† ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ Ų…ÛŒâ€ŒØ¨Ø§Ø´Ø¯.", + "backup_onboarding_3_description": "Ų…ØŦŲ…ŲˆØš ÚŠŲžÛŒâ€ŒŲ‡Ø§ÛŒ Ø¯Ø§Ø¯Ų‡â€ŒŲ‡Ø§ÛŒ Ø´Ų…Ø§ØŒ Ø¨Ų‡ Ų‡Ų…ØąØ§Ų‡ ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ÛŒ اØĩŲ„ÛŒ. Ø§ÛŒŲ† Ø´Ø§Ų…Ų„ Ûą ÚŠŲžÛŒ ØŽØ§ØąØŦی ؈ Û˛ ÚŠŲžÛŒ Ų…Ø­Ų„ÛŒ Ų…ÛŒâ€ŒØ¨Ø§Ø´Ø¯.", + "backup_onboarding_description": "Ø¨ØąØ§ÛŒ Ø­ŲØ§Ø¸ØĒ Ø§Ø˛ Ø§ØˇŲ„Ø§ØšØ§ØĒ Ø´Ų…Ø§ یڊ ØąŲˆØ´ ŲžØ´ØĒÛŒØ¨Ø§Ų†ÛŒ Ûŗ-Û˛-Ûą ŲžÛŒØ´Ų†Ų‡Ø§Ø¯ Ų…ÛŒâ€ŒØ´ŲˆØ¯. Ø¨ØąØ§ÛŒ یڊ ŲžØ´ØĒÛŒØ¨Ø§Ų†ÛŒ ØŦØ§Ų…ØšØŒ Ø´Ų…Ø§ باید ÚŠŲžÛŒâ€ŒŲ‡Ø§ÛŒÛŒ Ø§Ø˛ ØšÚŠØŗâ€ŒŲ‡Ø§/ŲˆÛŒØ¯ÛŒŲˆŲ‡Ø§ÛŒ ØĸŲžŲ„ŲˆØ¯ Ø´Ø¯Ų‡ ØŽŲˆØ¯ Ø¨Ų‡ Ų‡Ų…ØąØ§Ų‡ دیØĒØ§Ø¨ÛŒØŗ Immich Ų†Ú¯Ų‡ Ø¯Ø§ØąÛŒØ¯.", "backup_onboarding_footer": "Ø¨ØąØ§ÛŒ Ø§ØˇŲ„Ø§ØšØ§ØĒ بیشØĒØą Ø¯ØąØ¨Ø§ØąŲ‡ بڊ ØĸŲž Ú¯ÛŒØąÛŒ Ø§Ø˛ Immich، Ų„ØˇŲØ§ Ø¨Ų‡ Ų…ØŗØĒŲ†Ø¯Ø§ØĒ Ų…ØąØ§ØŦØšŲ‡ ÚŠŲ†ÛŒØ¯.", + "backup_onboarding_parts_title": "ØąŲˆØ´ ŲžØ´ØĒÛŒØ¨Ø§Ų†ÛŒ Ûŗ-Û˛-Ûą Ø´Ø§Ų…Ų„:", "backup_onboarding_title": "بڊ ØĸŲž Ų‡Ø§", + "backup_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ÚŠŲžÛŒâ€ŒØ¨ØąØ¯Ø§ØąÛŒ Ø§Ø˛ دیØĒØ§Ø¨ÛŒØŗ", + "backup_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ÚŠŲžÛŒâ€ŒØ¨ØąØ¯Ø§ØąØ¨ÛŒ Ø§Ø˛ دیØĒØ§Ø¨ÛŒØŗ.", "cleared_jobs": "ŲˆØ¸Ø§ÛŒŲ ŲžØ§ÚŠ Ø´Ø¯Ų‡ Ø¨ØąØ§ÛŒ:{job}", "config_set_by_file": "ØĒŲ†Ø¸ÛŒŲ… ŲØšŲ„ÛŒ ØĒŲˆØŗØˇ یڊ ŲØ§ÛŒŲ„ ŲžÛŒÚŠØąØ¨Ų†Ø¯ÛŒ Ø§Ų†ØŦØ§Ų… Ø´Ø¯Ų‡ Ø§ØŗØĒ", "confirm_delete_library": "Øĸیا Ų…ØˇŲ…ØĻŲ† Ų‡ØŗØĒید ÚŠŲ‡ Ų…ÛŒâ€ŒØŽŲˆØ§Ų‡ÛŒØ¯ ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ {library} ØąØ§ Ø­Ø°Ų ÚŠŲ†ÛŒØ¯ØŸ", @@ -365,7 +385,7 @@ "user_successfully_removed": "ÚŠØ§ØąØ¨Øą {email} با Ų…ŲˆŲŲ‚ÛŒØĒ Ø­Ø°Ų شد.", "users_page_description": "ØĩŲØ­Ų‡ Ų…Ø¯ÛŒØąÛŒØĒ ÚŠØ§ØąØ¨ØąØ§Ų†", "version_check_enabled_description": "ŲØšØ§Ų„â€ŒØŗØ§Ø˛ÛŒ Ø¨ØąØąØŗÛŒ Ų†ØŗØŽŲ‡", - "version_check_implications": "ŲˆÛŒÚ˜Ú¯ÛŒ Ø¨ØąØąØŗÛŒ Ų†ØŗØŽŲ‡ Ø¨Ų‡ Ø§ØąØĒØ¨Ø§Øˇ Ø¯ŲˆØąŲ‡ ای با github.com Ų…ØĒÚŠÛŒ Ø§ØŗØĒ", + "version_check_implications": "ŲˆÛŒÚ˜Ú¯ÛŒ Ø¨ØąØąØŗÛŒ Ų†ØŗØŽŲ‡ Ø¨Ų‡ Ø§ØąØĒØ¨Ø§Øˇ Ø¯ŲˆØąŲ‡ ای با {server} Ų…ØĒÚŠÛŒ Ø§ØŗØĒ", "version_check_settings": "Ø¨ØąØąØŗÛŒ Ų†ØŗØŽŲ‡", "version_check_settings_description": "ŲØšØ§Ų„ یا ØēÛŒØąŲØšØ§Ų„ ÚŠØąØ¯Ų† Ø§ØšŲ„Ø§Ų† Ų†ØŗØŽŲ‡ ØŦدید", "video_conversion_job": "ØĒØ¨Ø¯ÛŒŲ„ (ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ) ŲˆÛŒØ¯ÛŒŲˆŲ‡Ø§", diff --git a/i18n/fi.json b/i18n/fi.json index 084540324a..aaa2ee2bc1 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -441,7 +441,7 @@ "user_successfully_removed": "Käyttäjä {email} on poistettu onnistuneesti.", "users_page_description": "Ylläpitäjän käyttäjien lista", "version_check_enabled_description": "Ota käyttÃļÃļn versiotarkastus", - "version_check_implications": "Versiotarkistus vaatii säännÃļllisen yhteyden github.comiin", + "version_check_implications": "Versiotarkistus vaatii säännÃļllisen yhteyden {server}iin", "version_check_settings": "Versiotarkistus", "version_check_settings_description": "Ota käyttÃļÃļn ilmoitukset, kun uusi versio on saatavilla", "video_conversion_job": "Transkoodaa videot", @@ -849,9 +849,12 @@ "create_link_to_share": "Luo linkki jaettavaksi", "create_link_to_share_description": "Salli kaikkien linkin saaneiden nähdä valitut kuvat", "create_new": "LUO UUSI", + "create_new_face": "Luo uudet kasvot", "create_new_person": "Luo uusi henkilÃļ", "create_new_person_hint": "Määritä valitut mediat uudelle henkilÃļlle", "create_new_user": "Luo uusi käyttäjä", + "create_person": "Luo henkilÃļ", + "create_person_subtitle": "Lisää nimi valituille kasvoille luodaksesi uudelle henkilÃļlle tunnisteen", "create_shared_album_page_share_add_assets": "LISÄÄ KOHTEITA", "create_shared_album_page_share_select_photos": "Valitse kuvat", "create_shared_link": "Luo jakolinkki", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "Kiinteä", "crop_aspect_ratio_free": "Vapaa", "crop_aspect_ratio_original": "Alkuperäinen", + "crop_aspect_ratio_square": "NeliÃļ", "curated_object_page_title": "Asiat", "current_device": "Nykyinen laite", "current_pin_code": "Nykyinen PIN-koodi", @@ -880,7 +884,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Tumma", - "dark_theme": "Vaihda tumma teema", + "dark_theme": "Vaihda tummaan teemaan", "date": "Päivämäärä", "date_after": "Päivämäärän jälkeen", "date_and_time": "Päivämäärä ja aika", @@ -891,10 +895,8 @@ "day": "Päivä", "days": "Päivää", "deduplicate_all": "Poista kaikkien kaksoiskappaleet", - "deduplication_criteria_1": "Kuvan koko tavuina", - "deduplication_criteria_2": "EXIF-datan määrä", - "deduplication_info": "Deduplikaatiotieto", - "deduplication_info_description": "Jotta voimme automaattisesti esivalita aineistot ja poistaa kaksoiskappaleet suurina erinä, tarkastelemme:", + "default_locale": "Oletuskieli", + "default_locale_description": "Muotoile päivämäärät ja luvut selaimesi kieliasetusten mukaan", "delete": "Poista", "delete_action_confirmation_message": "Haluatko varmasti poistaa tämän aineiston? Tämä toiminto siirtää aineiston palvelimen roskakoriin ja kysyy, haluatko poistaa sen myÃļs paikallisesti", "delete_action_prompt": "{count} poistettu", @@ -970,7 +972,7 @@ "downloading_media": "Median lataaminen", "drop_files_to_upload": "Pudota tiedostot mihin tahansa ladataksesi ne", "duplicates": "Kaksoiskappaleet", - "duplicates_description": "Selvitä jokaisen kohdalla mitkä (jos mitkään) ovat kaksoiskappaleita", + "duplicates_description": "Selvitä jokaisen kohdalla mitkä (jos mitkään) ovat kaksoiskappaleita.", "duration": "Kesto", "edit": "Muokkaa", "edit_album": "Muokkaa albumia", @@ -1007,6 +1009,8 @@ "editor_edits_applied_success": "Muutokset otettu käyttÃļÃļn", "editor_flip_horizontal": "Käännä vaakatasossa", "editor_flip_vertical": "Käännä pystytasossa", + "editor_handle_corner": "{corner, select, top_left {Vasen yläkulma} top_right {Oikea yläkulma} bottom_left {Vasen alakulma} bottom_right {Oikea alakulma} other {A}} kulman kahva", + "editor_handle_edge": "{edge, select, top {Yläreuna} bottom {Alareuna} left {Vasen reuna} right {Oikea reuna} other {En}} reunan kahva", "editor_orientation": "Suunta", "editor_reset_all_changes": "Nollaa muutokset", "editor_rotate_left": "Kierrä 90° vastapäivään", @@ -1385,9 +1389,11 @@ "library_page_sort_title": "Albumin otsikko", "licenses": "Lisenssit", "light": "Vaalea", + "light_theme": "Vaihda vaaleaan teemaan", "like": "Tykkää", "like_deleted": "Tykkäys poistettu", "link_motion_video": "Linkitä liikevideo", + "link_to_docs": "Lisätietoja lÃļytyy dokumentaatiosta.", "link_to_oauth": "Linkki OAuth", "linked_oauth_account": "Linkitetty OAuth-tili", "list": "Lista", @@ -2211,6 +2217,7 @@ "tag": "Tunniste", "tag_assets": "Lisää tunnisteita", "tag_created": "Luotu tunniste: {tag}", + "tag_face": "Merkitse kasvot", "tag_feature_description": "Selaa valokuvia ja videoita, jotka on ryhmitelty loogisten tunnisteotsikoiden mukaan", "tag_not_found_question": "EtkÃļ lÃļydä tunnistetta? Luo uusi tunniste.", "tag_people": "Merkitse henkilÃļ tunnisteella", @@ -2392,6 +2399,7 @@ "viewer_remove_from_stack": "Poista pinosta", "viewer_stack_use_as_main_asset": "Käytä pääkohteena", "viewer_unstack": "Pura pino", + "visibility": "Näkyvyys", "visibility_changed": "{count, plural, one {# henkilÃļn} other {# henkilÃļiden}} näkyvyys vaihdettu", "visual": "Visuaalinen", "visual_builder": "Visuaalinen koostaja", diff --git a/i18n/fr.json b/i18n/fr.json index ddbbe1dd13..f15931b0d6 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -441,7 +441,7 @@ "user_successfully_removed": "L'utilisateur {email} a ÊtÊ supprimÊ avec succès.", "users_page_description": "Page d'administration des utilisateurs", "version_check_enabled_description": "Activer la vÊrification pÊriodique de nouvelle version", - "version_check_implications": "Le contrôle de version repose sur une communication pÊriodique avec github.com", + "version_check_implications": "Le contrôle de version repose sur une communication pÊriodique avec {server}", "version_check_settings": "VÊrification de la version", "version_check_settings_description": "GÊrer la vÊrification de nouvelle version d'Immich", "video_conversion_job": "Transcodage des vidÊos", @@ -849,9 +849,12 @@ "create_link_to_share": "CrÊer un lien pour partager", "create_link_to_share_description": "Permettre à n'importe qui ayant le lien de voir la(es) photo(s) sÊlectionnÊe(s)", "create_new": "NOUVEAU", + "create_new_face": "CrÊer un nouveau visage", "create_new_person": "CrÊer une nouvelle personne", "create_new_person_hint": "Attribuer les mÊdias sÊlectionnÊs à une nouvelle personne", "create_new_user": "CrÊer un nouvel utilisateur", + "create_person": "CrÊer une personne", + "create_person_subtitle": "Ajouter un nom au visage sÊlectionnÊ pour crÊer et Êtiqueter la nouvelle personne", "create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS", "create_shared_album_page_share_select_photos": "SÊlectionner les photos", "create_shared_link": "CrÊer un lien partagÊ", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "FigÊ", "crop_aspect_ratio_free": "Libre", "crop_aspect_ratio_original": "Original", + "crop_aspect_ratio_square": "CarrÊ", "curated_object_page_title": "Objets", "current_device": "Appareil actuel", "current_pin_code": "Code PIN actuel", @@ -880,7 +884,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Sombre", - "dark_theme": "Activer le thème sombre", + "dark_theme": "Basculer sur le thème sombre", "date": "Date", "date_after": "Date après", "date_and_time": "Date et heure", @@ -891,10 +895,8 @@ "day": "Jour", "days": "Jours", "deduplicate_all": "DÊdupliquer tout", - "deduplication_criteria_1": "Taille de l'image en octets", - "deduplication_criteria_2": "Nombre de donnÊes EXIF", - "deduplication_info": "Info de dÊduplication", - "deduplication_info_description": "Pour prÊsÊlectionner automatiquement les mÊdias et supprimer les doublons en masse, nous examinons :", + "default_locale": "Langue par dÊfaut", + "default_locale_description": "Mettre en forme les dates et nombres en fonction de la langue de votre navigateur", "delete": "Supprimer", "delete_action_confirmation_message": "Êtes-vous sÃģr de vouloir supprimer ce mÊdia ? Cela dÊplacera le mÊdia dans la poubelle du serveur et vous demandera si vous voulez le supprimer localement", "delete_action_prompt": "{count} supprimÊ(s)", @@ -970,7 +972,7 @@ "downloading_media": "TÊlÊchargement du mÊdia", "drop_files_to_upload": "DÊposez les fichiers n'importe oÚ pour envoyer", "duplicates": "Doublons", - "duplicates_description": "Examiner chaque groupe et indiquer s'il y a des doublons", + "duplicates_description": "Examiner chaque groupe et indiquer s'il y a des doublons.", "duration": "DurÊe", "edit": "Modifier", "edit_album": "Modifier l'album", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "Titre de l'album", "licenses": "Licences", "light": "Clair", + "light_theme": "Basculer sur le thème clair", "like": "J'aime", "like_deleted": "RÊaction ÂĢ J'aime Âģ supprimÊe", "link_motion_video": "Lier la photo animÊe", + "link_to_docs": "Pour plus d'informations, se rÊfÊrer à la documentation.", "link_to_oauth": "Lien au service OAuth", "linked_oauth_account": "Compte OAuth rattachÊ", "list": "Liste", @@ -2213,6 +2217,7 @@ "tag": "Étiquette", "tag_assets": "Étiqueter les mÊdias", "tag_created": "Étiquette crÊÊe : {tag}", + "tag_face": "Étiqueter le visage", "tag_feature_description": "Parcourir les photos et vidÊos groupÊes par thèmes logiques", "tag_not_found_question": "Vous ne trouvez pas une Êtiquette ? CrÊer une nouvelle Êtiquette.", "tag_people": "Étiqueter les personnes", @@ -2394,6 +2399,7 @@ "viewer_remove_from_stack": "Retirer de la pile", "viewer_stack_use_as_main_asset": "Utiliser comme ÊlÊment principal", "viewer_unstack": "DÊpiler", + "visibility": "VisibilitÊ", "visibility_changed": "VisibilitÊ changÊe pour {count, plural, one {# personne} other {# personnes}}", "visual": "Visuel", "visual_builder": "Constructeur visuel", diff --git a/i18n/ga.json b/i18n/ga.json index 8cdcf03fbe..d493e3fe23 100644 --- a/i18n/ga.json +++ b/i18n/ga.json @@ -441,7 +441,7 @@ "user_successfully_removed": "Baineadh an t-ÃēsÃĄideoir {email} go rathÃēil.", "users_page_description": "Leathanach ÃēsÃĄideoirí riarthÃŗra", "version_check_enabled_description": "Cumasaigh seiceÃĄil leagan", - "version_check_implications": "Braitheann an ghnÊ seiceÃĄla leagan ar chumarsÃĄid thrÊimhsiÃēil le github.com", + "version_check_implications": "Braitheann an ghnÊ seiceÃĄla leagan ar chumarsÃĄid thrÊimhsiÃēil le {server}", "version_check_settings": "SeiceÃĄil Leagan", "version_check_settings_description": "Cumasaigh/díchumasaigh an fÃŗgra faoin leagan nua", "video_conversion_job": "FíseÃĄin TraschÃŗdaithe", @@ -849,9 +849,12 @@ "create_link_to_share": "Cruthaigh nasc le roinnt", "create_link_to_share_description": "Lig do dhuine ar bith a bhfuil an nasc aige/aici an/na grianghraf/na grianghraif roghnaithe a fheiceÃĄil", "create_new": "CRUTHAIGH NUA", + "create_new_face": "Cruthaigh aghaidh nua", "create_new_person": "Cruthaigh duine nua", "create_new_person_hint": "Sannadh sÃŗcmhainní roghnaithe do dhuine nua", "create_new_user": "Cruthaigh ÃēsÃĄideoir nua", + "create_person": "Cruthaigh duine", + "create_person_subtitle": "Cuir ainm leis an aghaidh roghnaithe chun an duine nua a chruthÃē agus a chlibeÃĄil", "create_shared_album_page_share_add_assets": "CUIR SÓCMHAINNÍ LEIS", "create_shared_album_page_share_select_photos": "Roghnaigh Grianghraif", "create_shared_link": "Cruthaigh nasc comhroinnte", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "Seasta", "crop_aspect_ratio_free": "Saor in aisce", "crop_aspect_ratio_original": "Bunaidh", + "crop_aspect_ratio_square": "CearnÃŗg", "curated_object_page_title": "Rudaí", "current_device": "GlÊas reatha", "current_pin_code": "CÃŗd PIN reatha", @@ -880,7 +884,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Dorcha", - "dark_theme": "ScorÃĄnaigh an tÊama dorcha", + "dark_theme": "Athraigh go tÊama dorcha", "date": "DÃĄta", "date_after": "DÃĄta i ndiaidh", "date_and_time": "DÃĄta agus Am", @@ -891,10 +895,8 @@ "day": "LÃĄ", "days": "Laethanta", "deduplicate_all": "DídhÃēblaigh Gach Rud", - "deduplication_criteria_1": "MÊid na híomhÃĄ i mbÊiteanna", - "deduplication_criteria_2": "Líon sonraí EXIF", - "deduplication_info": "Eolas DídhÃēblÃĄla", - "deduplication_info_description": "Chun sÃŗcmhainní a rÊamhroghnÃē go huathoibríoch agus dÃēblaigh a bhaint i mÃŗrchÃŗir, fÊachaimid ar:", + "default_locale": "LogÃĄn RÊamhshocraithe", + "default_locale_description": "FormÃĄidigh dÃĄtaí agus uimhreacha bunaithe ar shuíomh do bhrabhsÃĄlaí", "delete": "Scrios", "delete_action_confirmation_message": "An bhfuil tÃē cinnte gur mian leat an tsÃŗcmhainn seo a scriosadh? Bogfaidh an gníomh seo an tsÃŗcmhainn go dtí bruscar an fhreastalaí agus fiafrÃŗidh sÊ díot an mian leat í a scriosadh go hÃĄitiÃēil", "delete_action_prompt": "{count} scriosta", @@ -970,7 +972,7 @@ "downloading_media": "Ag íoslÃŗdÃĄil na meÃĄn", "drop_files_to_upload": "Scaoil comhaid ÃĄit ar bith le huaslÃŗdÃĄil", "duplicates": "DÃēblaigh", - "duplicates_description": "RÊitigh gach grÃēpa trína lÊiriÃē cÊ acu de na dÃēblaigh, mÃĄs ann dÃŗibh", + "duplicates_description": "RÊitigh gach grÃēpa trína lÊiriÃē cÊ acu de na dÃēblaigh, mÃĄs ann dÃŗibh.", "duration": "Fad", "edit": "Cuir in Eagar", "edit_album": "Cuir albam in eagar", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "Teideal an albaim", "licenses": "CeadÃēnais", "light": "Solas", + "light_theme": "Athraigh go tÊama Êadrom", "like": "Is maith liom", "like_deleted": "Scriosadh an rud is maith liom", "link_motion_video": "FíseÃĄn gluaiseachta nasctha", + "link_to_docs": "Le haghaidh tuilleadh eolais, fÊach ar an doicimÊadÃē.", "link_to_oauth": "Nasc le OAuth", "linked_oauth_account": "Cuntas OAuth nasctha", "list": "Liosta", @@ -2213,6 +2217,7 @@ "tag": "Clib", "tag_assets": "SÃŗcmhainní clibe", "tag_created": "Clib cruthaithe: {tag}", + "tag_face": "Aghaidh clibe", "tag_feature_description": "Ag brabhsÃĄil grianghraif agus físeÃĄin grÃēpÃĄilte de rÊir topaicí clibeanna loighciÃēla", "tag_not_found_question": "Ní fÊidir clib a aimsiÃē? Cruthaigh clib nua.", "tag_people": "Daoine a ChlibeÃĄil", @@ -2394,6 +2399,7 @@ "viewer_remove_from_stack": "Bain den Chruach", "viewer_stack_use_as_main_asset": "ÚsÃĄid mar PhríomhshÃŗcmhainn", "viewer_unstack": "Dí-Chruach", + "visibility": "Infheictheacht", "visibility_changed": "Athraíodh infheictheacht do {count, plural, one {# duine} other {# daoine}}", "visual": "Amhairc", "visual_builder": "TÃŗgÃĄlaí amhairc", diff --git a/i18n/gl.json b/i18n/gl.json index 63faaec9a8..201f718998 100644 --- a/i18n/gl.json +++ b/i18n/gl.json @@ -441,7 +441,7 @@ "user_successfully_removed": "O usuario {email} foi eliminado satisfactoriamente.", "users_page_description": "PÃĄxina de usuarios administradores", "version_check_enabled_description": "Activar comprobaciÃŗn de versiÃŗn", - "version_check_implications": "A funciÃŗn de comprobaciÃŗn de versiÃŗn depende da comunicaciÃŗn periÃŗdica con github.com", + "version_check_implications": "A funciÃŗn de comprobaciÃŗn de versiÃŗn depende da comunicaciÃŗn periÃŗdica con {server}", "version_check_settings": "ComprobaciÃŗn de VersiÃŗn", "version_check_settings_description": "Activar/desactivar a notificaciÃŗn de nova versiÃŗn", "video_conversion_job": "Transcodificar vídeos", @@ -849,9 +849,12 @@ "create_link_to_share": "Crear ligazÃŗn para compartir", "create_link_to_share_description": "Permitir que calquera persoa coa ligazÃŗn vexa a(s) foto(s) seleccionada(s)", "create_new": "CREAR NOVO", + "create_new_face": "Crear nova cara", "create_new_person": "Crear nova persoa", "create_new_person_hint": "Asignar activos seleccionados a unha nova persoa", "create_new_user": "Crear novo usuario", + "create_person": "Crear persona", + "create_person_subtitle": "Engade un nome ÃĄ cara seleccionada para crear e etiquetar ÃĄ nova persona", "create_shared_album_page_share_add_assets": "ENGADIR ACTIVOS", "create_shared_album_page_share_select_photos": "Seleccionar Fotos", "create_shared_link": "Crear ligazÃŗn compartida", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "Fixado", "crop_aspect_ratio_free": "Libre", "crop_aspect_ratio_original": "Orixinal", + "crop_aspect_ratio_square": "Cadrado", "curated_object_page_title": "Cousas", "current_device": "Dispositivo actual", "current_pin_code": "CÃŗdigo PIN actual", @@ -880,7 +884,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Escuro", - "dark_theme": "Alternar tema escuro", + "dark_theme": "Alternar a tema escuro", "date": "Data", "date_after": "Data posterior a", "date_and_time": "Data e Hora", @@ -891,10 +895,8 @@ "day": "Día", "days": "Días", "deduplicate_all": "Eliminar todos os duplicados", - "deduplication_criteria_1": "TamaÃąo da imaxe en bytes", - "deduplication_criteria_2": "Reconto de datos EXIF", - "deduplication_info": "InformaciÃŗn de DeduplicaciÃŗn", - "deduplication_info_description": "Para preseleccionar automaticamente activos e eliminar duplicados masivamente, miramos:", + "default_locale": "ConfiguraciÃŗn rexional predeterminada", + "default_locale_description": "Formatee as datas e os nÃēmeros segÃēn a configuraciÃŗn rexional do seu navegador", "delete": "Eliminar", "delete_action_confirmation_message": "EstÃĄ seguro de que quere eliminar este ficheiro? Esta acciÃŗn moverÃĄ o ficheiro ao lixo do servidor e preguntaralle se tamÊn quere eliminalo localmente", "delete_action_prompt": "{count} eliminado(s)", @@ -970,7 +972,7 @@ "downloading_media": "Descargando medios", "drop_files_to_upload": "Solte ficheiros en calquera lugar para cargar", "duplicates": "Duplicados", - "duplicates_description": "Resolve cada grupo indicando cales, se os houber, son duplicados", + "duplicates_description": "Resolve cada grupo indicando cales, se os houber, son duplicados.", "duration": "DuraciÃŗn", "edit": "Editar", "edit_album": "Editar ÃĄlbum", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "Título do ÃĄlbum", "licenses": "Licenzas", "light": "Claro", + "light_theme": "Cambiar a tema claro", "like": "GÃēstame", "like_deleted": "GÃēstame eliminado", "link_motion_video": "Ligar vídeo en movemento", + "link_to_docs": "Para mÃĄis informaciÃŗn, consulte a documentaciÃŗn.", "link_to_oauth": "Ligar a OAuth", "linked_oauth_account": "Conta OAuth ligada", "list": "Lista", @@ -2213,6 +2217,7 @@ "tag": "Etiqueta", "tag_assets": "Etiquetar activos", "tag_created": "Etiqueta creada: {tag}", + "tag_face": "Etiquetar cara", "tag_feature_description": "Navegar por fotos e vídeos agrupados por temas de etiquetas lÃŗxicas", "tag_not_found_question": "Non atopa unha etiqueta? Crear unha nova etiqueta.", "tag_people": "Etiquetar Persoas", @@ -2394,6 +2399,7 @@ "viewer_remove_from_stack": "Eliminar da Pila", "viewer_stack_use_as_main_asset": "Usar como Activo Principal", "viewer_unstack": "Desapilar", + "visibility": "Visibilidade", "visibility_changed": "Visibilidade cambiada para {count, plural, one {# persoa} other {# persoas}}", "visual": "Visual", "visual_builder": "Construtor visual", diff --git a/i18n/gsw.json b/i18n/gsw.json index bad2816ad9..7893ea1482 100644 --- a/i18n/gsw.json +++ b/i18n/gsw.json @@ -422,7 +422,7 @@ "user_successfully_removed": "Dr Benutzer {email} isch erfolgrich entfernt worde.", "users_page_description": "Administrator-Benutzersiite", "version_check_enabled_description": "VersionsprÃŧefig akivierä", - "version_check_implications": "D’Funktion zur VersionsprÃŧefig basiert uf regelmässiger Kommunikazion mit GitHub.com", + "version_check_implications": "D’Funktion zur VersionsprÃŧefig basiert uf regelmässiger Kommunikazion mit {server}", "version_check_settings": "VersionsprÃŧefig", "version_check_settings_description": "Aktiviere/Deaktivier d’Benochrichtigung Ãŧber neui Versione", "video_conversion_job": "Videos transkodiere", @@ -835,10 +835,6 @@ "day": "Tag", "days": "Täg", "deduplicate_all": "Alli Duplikate entfernä", - "deduplication_criteria_1": "BildgrÃļssi in Bytes", - "deduplication_criteria_2": "Anzahl vo de EXIF Date", - "deduplication_info": "Deduplizierungsinformatione", - "deduplication_info_description": "FÃŧr d’automatischi Datei-Voruswahl und s’Dedupliziere vo allne Dateie berÃŧcksichtige mir:", "delete": "LÃļsche", "delete_action_confirmation_message": "Bisch du sicher, dass du dies Objekt lÃļsche wotsch? Die Aktion verschiebt s’Objekt i de Papirkorb vom Server und fragt dich, ob du’s lokal lÃļÃļsche wotsch", "delete_action_prompt": "{count} glÃļscht", diff --git a/i18n/he.json b/i18n/he.json index 629f8166cb..3169c356ec 100644 --- a/i18n/he.json +++ b/i18n/he.json @@ -20,7 +20,7 @@ "add_action_description": "לח×Ĩ כדי ×œ×”×•×Ą×™×Ŗ פ×ĸולה לבי×Ļו×ĸ", "add_assets": "×”×•×Ą×Ŗ ×Ēמונו×Ē", "add_birthday": "הוספ×Ē ×™×•× הולד×Ē", - "add_endpoint": "×”×•×Ą×Ŗ כ×Ēוב×Ē URL", + "add_endpoint": "הוספ×Ē ×›×Ēוב×Ē ×§×Ļה", "add_exclusion_pattern": "הוספ×Ē ×“×¤×•×Ą החרגה", "add_filter": "×”×•×Ą×Ŗ סינון", "add_filter_description": "לח×Ĩ כדי ×œ×”×•×Ą×™×Ŗ ×Ēנאי לסינון", @@ -53,7 +53,7 @@ "authentication_settings": "הגדרו×Ē ×”×Ēחברו×Ē", "authentication_settings_description": "ניהול סיסמה, OAuth, והגדרו×Ē ×”×Ēחברו×Ē ××—×¨×•×Ē", "authentication_settings_disable_all": "האם בר×Ļונך להשבי×Ē ××Ē ×›×œ שיטו×Ē ×”×”×Ēחברו×Ē? כניסה למ×ĸרכ×Ē ×Ēהיה מושב×Ē×Ē ×œ×—×œ×•×˜×™×Ÿ.", - "authentication_settings_reenable": "כדי לאפשר מחדש, יש להש×Ēמ׊ בפקוד×Ē ×Š×¨×Ē.", + "authentication_settings_reenable": "כדי לאפשר מחדש, יש להש×Ēמ׊ בפקוד×Ē ×Š×¨×Ē.", "background_task_job": "משימו×Ē ×¨×§×ĸ", "backup_database": "גיבוי מסד × ×Ēונים", "backup_database_enable_description": "אפ׊ר גיבויי מסד × ×Ēונים", @@ -62,7 +62,7 @@ "backup_onboarding_2_description": "ה×ĸ×Ēקים מקומיים במכשירים שונים. זה כולל א×Ē ×”×§×‘×Ļים הראשיים וגיבוי של הקב×Ļים האלה באופן מקומי.", "backup_onboarding_3_description": "סך כל הה×ĸ×Ēקים של הנ×Ēונים שלך, כולל הקב×Ļים המקוריים. זה כולל ה×ĸ×Ē×§ אחד מחו×Ĩ למקום השר×Ē ×•×Š× ×™ ה×ĸ×Ēקים מקומיים.", "backup_onboarding_description": "אסטרטגיי×Ē ×’×™×‘×•×™ 3-2-1 הינה מומל×Ļ×Ē ×ĸל מנ×Ē ×œ×”×’×Ÿ ×ĸל הנ×Ēונים שלך. ×ĸליך להשאיר ה×ĸ×Ēקים של ×Ēמונו×Ē/סרטונים שהו×ĸלו כמו גם א×Ē ×ž×Ą×“ הנ×Ēונים של Immich ×ĸבור פ×Ēרון גיבוי ×ž×§×™×Ŗ.", - "backup_onboarding_footer": "×ĸבור מיד×ĸ × ×•×Ą×Ŗ ×ĸל גיבוי Immich, נא לפנו×Ē ××œ ה×Ēי×ĸוד.", + "backup_onboarding_footer": "×ĸבור מיד×ĸ × ×•×Ą×Ŗ ×ĸל גיבוי Immich, נא לפנו×Ē ××œ ה×Ēי×ĸוד.", "backup_onboarding_parts_title": "גיבוי 3-2-1 כולל:", "backup_onboarding_title": "גיבויים", "backup_settings": "הגדרו×Ē ×’×™×‘×•×™", @@ -281,7 +281,7 @@ "oauth_role_claim_description": "ה×ĸ× ×§ גיש×Ē ×ž× ×”×œ באופן אוטומטי אם ×Ēבי×ĸה זו קיימ×Ē. ×ĸרך ה×Ēבי×ĸה יכול להיו×Ē 'user' או 'admin'.", "oauth_settings": "OAuth", "oauth_settings_description": "ניהול הגדרו×Ē ×”×Ēחברו×Ē ×ĸם OAuth", - "oauth_settings_more_details": "למיד×ĸ × ×•×Ą×Ŗ אודו×Ē ×Ēכונה זו, בדוק א×Ē ×”×Ēי×ĸוד.", + "oauth_settings_more_details": "למיד×ĸ × ×•×Ą×Ŗ אודו×Ē ×Ēכונה זו, בדוק א×Ē ×”×Ēי×ĸוד.", "oauth_storage_label_claim": "דריש×Ē ×Ēווי×Ē ××—×Ą×•×Ÿ", "oauth_storage_label_claim_description": "הגדר אוטומטי×Ē ××Ē ×Ēווי×Ē ×”××—×Ą×•×Ÿ של המש×Ēמ׊ ל×ĸרך של דרישה זו.", "oauth_storage_quota_claim": "דריש×Ē ×ž×›×Ą×Ē ××—×Ą×•×Ÿ", @@ -330,7 +330,7 @@ "storage_template_hash_verification_enabled": "אימו×Ē ×’×™×‘×•×‘ מופ×ĸל", "storage_template_hash_verification_enabled_description": "מאפ׊ר אימו×Ē ×’×™×‘×•×‘, אין להשבי×Ē ×–××Ē ××œ× אם יש לך ודאו×Ē ×œ×’×‘×™ ההשלכו×Ē", "storage_template_migration": "ה×ĸבר×Ē ×Ēבני×Ē ××—×Ą×•×Ÿ", - "storage_template_migration_description": "החל א×Ē ×”{template} הנוכחי×Ē ×ĸל ×Ēמונו×Ē ×Š×”×•×ĸלו ב×ĸבר", + "storage_template_migration_description": "החל×Ē ×”{template} הנוכחי ×ĸל ×Ēמונו×Ē ×Š×”×•×ĸלו ב×ĸבר", "storage_template_migration_info": "×Ēבני×Ē ×”××—×Ą×•×Ÿ ×Ēמיר א×Ē ×›×œ ההרחבו×Ē ×œ××•×Ēיו×Ē ×§×˜× ×•×Ē. שינויים ב×Ēבני×Ē ×™×—×•×œ×• רק ×ĸל ×Ēמונו×Ē ×—×“×Š×•×Ē. כדי להחיל באופן רטרואקטיבי א×Ē ×”×Ēבני×Ē ×ĸל ×Ēמונו×Ē ×Š×”×•×ĸלו ב×ĸבר, הפ×ĸל א×Ē {job}.", "storage_template_migration_job": "משימ×Ē ×”×ĸבר×Ē ×Ēבני×Ē ××—×Ą×•×Ÿ", "storage_template_more_details": "לפרטים נוספים אודו×Ē ×Ēכונה זו, ×ĸיין ב×Ēבני×Ē ×”××—×Ą×•×Ÿ ובהשלכו×Ēיה", @@ -441,7 +441,7 @@ "user_successfully_removed": "המש×Ēמ׊ {email} הוסר בה×Ļלחה.", "users_page_description": "×ĸמוד ניהול מ׊×Ēמשים", "version_check_enabled_description": "אפ׊ר בדיק×Ē ×’×¨×Ą×”", - "version_check_implications": "×Ēכונ×Ē ×‘×“×™×§×Ē ×”×’×¨×Ą×” מס×Ēמכ×Ē ×ĸל ×Ēקשור×Ē ×Ēקופ×Ēי×Ē ×ĸם github.com", + "version_check_implications": "×Ēכונ×Ē ×‘×“×™×§×Ē ×”×’×¨×Ą×” מס×Ēמכ×Ē ×ĸל ×Ēקשור×Ē ×Ēקופ×Ēי×Ē ×ĸם {server}", "version_check_settings": "בדיק×Ē ×’×¨×Ą×”", "version_check_settings_description": "הפ×ĸל/השב×Ē ××Ē ×”×”×Ēראה ×ĸל גרסה חדשה", "video_conversion_job": "המר×Ē ×§×™×“×•×“ סרטונים", @@ -866,6 +866,7 @@ "crop_aspect_ratio_fixed": "×Ēוקן", "crop_aspect_ratio_free": "חינם", "crop_aspect_ratio_original": "מקורי", + "crop_aspect_ratio_square": "ריבו×ĸ", "curated_object_page_title": "דברים", "current_device": "מכשיר נוכחי", "current_pin_code": "קוד PIN הנוכחי", @@ -880,7 +881,7 @@ "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "כהה", - "dark_theme": "הפ×ĸל/כבה מ×Ļב כהה", + "dark_theme": "מ×ĸבר ל×ĸרכ×Ē × ×•×Š× כהה", "date": "×Ēאריך", "date_after": "×Ēאריך אחרי", "date_and_time": "×Ēאריך וש×ĸה", @@ -891,10 +892,8 @@ "day": "יום", "days": "ימים", "deduplicate_all": "ביטול כל הכפילויו×Ē", - "deduplication_criteria_1": "גודל ×Ēמונה בב×Ēים", - "deduplication_criteria_2": "כמו×Ē × ×Ēוני EXIF", - "deduplication_info": "מיד×ĸ ×ĸל ביטול כפילויו×Ē", - "deduplication_info_description": "כדי לבחור מרא׊ ×Ēמונו×Ē ×‘××•×¤×Ÿ אוטומטי ולהסיר כפילויו×Ē ×‘×›×ž×•×Ē ×’×“×•×œ×”, אנו מס×Ēכלים ×ĸל:", + "default_locale": "אזור שפה בריר×Ē ×ž×—×“×œ", + "default_locale_description": "×ĸי×Ļוב ×Ēאריכים ומספרים בה×Ēבסס ×ĸל אזור השפה של הדפדפן שלך", "delete": "מחק", "delete_action_confirmation_message": "האם א×Ēה בטוח שבר×Ļונך למחוק א×Ē ×”×Ēמונה הזא×Ē? פ×ĸולה זו ×Ē×ĸביר או×Ēו לאשפה של השר×Ē, ו×Ēשאל אם בר×Ļונך למחוק או×Ēו גם מהמכשיר המקומי", "delete_action_prompt": "{count} נמחקו", @@ -970,7 +969,7 @@ "downloading_media": "מוריד מדיה", "drop_files_to_upload": "שחרר קב×Ļים בכל מקום כדי לה×ĸלו×Ē", "duplicates": "כפילויו×Ē", - "duplicates_description": "הפרד כל קבו×Ļה ×ĸל ידי ×Ļיון אילו, אם בכלל, הן כפילויו×Ē", + "duplicates_description": "הפרד כל קבו×Ļה ×ĸל ידי ×Ļיון אילו, אם בכלל, הן כפילויו×Ē.", "duration": "משך זמן", "edit": "×ĸרוך", "edit_album": "×ĸרוך אלבום", @@ -1007,6 +1006,8 @@ "editor_edits_applied_success": "×ĸריכו×Ē ×™×•×Š×ž×• בה×Ļלחה", "editor_flip_horizontal": "הפוך אופקי×Ē", "editor_flip_vertical": "הפוך אנכי×Ē", + "editor_handle_corner": "ידי×Ē ×”×¤×™× ×” {corner, select, top_left {השמאלי×ĒÖž×ĸליונה} top_right {הימני×ĒÖž×ĸליונה} bottom_left {השמאלי×ĒÖž×Ēח×Ēונה} bottom_right {הימני×ĒÖž×Ēח×Ēונה} other {}}", + "editor_handle_edge": "ידי×Ē ×”×§×Ļה {edge, select, top {ה×ĸליון} bottom {ה×Ēח×Ēון} left {השמאלי} right {הימני} other {}}", "editor_orientation": "כיוון", "editor_reset_all_changes": "איפוס שינויים", "editor_rotate_left": "סיבוב 90° נגד כיוון הש×ĸון", @@ -1072,7 +1073,7 @@ "failed_to_update_notification_status": "שגיאה ב×ĸדכון הה×Ēראה", "incorrect_email_or_password": "דוא\"ל או סיסמה שגויים", "library_folder_already_exists": "× ×Ēיב הייבוא כבר מוגדר.", - "page_not_found": "ה×ĸמוד לא נמ×Ļא â€Ē:/â€Ŧ", + "page_not_found": "ה×ĸמוד לא נמ×Ļא", "paths_validation_failed": "{paths, plural, one {× ×Ēיב # נכשל} other {# × ×Ēיבים נכשלו}} אימו×Ē", "profile_picture_transparent_pixels": "×Ēמונו×Ē ×¤×¨×•×¤×™×œ אינן יכולו×Ē ×œ×›×œ×•×œ פיקסלים שקופים. נא להגדיל ו/או להזיז א×Ē ×”×Ēמונה.", "quota_higher_than_disk_size": "הגדר×Ē ×ž×›×Ą×” גבוהה יו×Ēר מגודל הדיסק", @@ -1385,9 +1386,11 @@ "library_page_sort_title": "כו×Ēר×Ē ××œ×‘×•×", "licenses": "רישיונו×Ē", "light": "בהיר", + "light_theme": "החלפה ל×ĸרכ×Ē × ×•×Š× בהירה", "like": "אהב×Ēי", "like_deleted": "לייק נמחק", "link_motion_video": "ק׊ר סרטון ×Ēנו×ĸה", + "link_to_docs": "למיד×ĸ × ×•×Ą×Ŗ, יש ל×ĸיין ב×Ēי×ĸוד.", "link_to_oauth": "קישור ל-OAuth", "linked_oauth_account": "חשבון OAuth מקושר", "list": "רשימה", @@ -1566,7 +1569,7 @@ "network_requirements": "דרישו×Ē ×¨×Š×Ē", "network_requirements_updated": "דרישו×Ē ×”×¨×Š×Ē ×”×Š×Ēנו, ×Ēור הגיבוי אופס", "networking_settings": "ר׊×Ē", - "networking_subtitle": "ניהול הגדרו×Ē ×›×Ēוב×Ē URL של השר×Ē", + "networking_subtitle": "ניהול הגדרו×Ē ×›×Ēוב×Ē ×”×Š×¨×Ē", "never": "את פ×ĸם", "new_album": "אלבום חדש", "new_api_key": "מפ×Ēח API חדש", @@ -1649,6 +1652,7 @@ "only_favorites": "רק מו×ĸדפים", "open": "פ×Ēח", "open_calendar": "פ×Ēיח×Ē ×œ×•×— שנה", + "open_in_browser": "פ×Ēיחה בדפדפן", "open_in_map_view": "פ×Ēח ב×Ē×Ļוג×Ē ×ž×¤×”", "open_in_openstreetmap": "פ×Ēח ב-OpenStreetMap", "open_the_search_filters": "פ×Ēח א×Ē ×ž×Ą× × ×™ החיפוש", @@ -2009,7 +2013,7 @@ "selected_gps_coordinates": "קואורדינטו×Ē GPS שנבחרו", "send_message": "שלח הוד×ĸה", "send_welcome_email": "שלח דוא\"ל קבל×Ē ×¤× ×™×", - "server_endpoint": "כ×Ēוב×Ē URL של השר×Ē", + "server_endpoint": "כ×Ēוב×Ē ×”×Š×¨×Ē", "server_info_box_app_version": "גרס×Ē ×™×™×Š×•×", "server_info_box_server_url": "כ×Ēוב×Ē ×Š×¨×Ē", "server_offline": "השר×Ē ×ž× ×•×Ē×§", @@ -2211,7 +2215,7 @@ "tag_assets": "×Ēיוג ×Ēמונו×Ē", "tag_created": "נו×Ļר ×Ēג: {tag}", "tag_feature_description": "×ĸיון ב×Ēמונו×Ē ×•×Ą×¨×˜×•× ×™× שקוב×Ļו ×ĸל ידי נושאי ×Ēג לוגיים", - "tag_not_found_question": "לא מ×Ļליח למ×Ļוא ×Ēג? ×Ļור ×Ēג חדש", + "tag_not_found_question": "לא ני×Ēן למ×Ļוא ×Ēג? י×Ļיר×Ē ×Ēג חדש.", "tag_people": "×Ēייג אנשים", "tag_updated": "×Ēג מ×ĸודכן: {tag}", "tagged_assets": "×Ēויגו {count, plural, one {×Ēמונה #} other {# ×Ēמונו×Ē}}", @@ -2391,6 +2395,7 @@ "viewer_remove_from_stack": "הסר מ×ĸרימה", "viewer_stack_use_as_main_asset": "הש×Ēמ׊ כ×Ēמונה ראשי×Ē", "viewer_unstack": "ביטול ×ĸרימה", + "visibility": "נראו×Ē", "visibility_changed": "הנראו×Ē ×”×Š×Ē× ×Ēה ×ĸבור {count, plural, one {אדם #} other {# אנשים}}", "visual": "חזו×Ēי", "visual_builder": "בונה חזו×Ēי", diff --git a/i18n/hi.json b/i18n/hi.json index c7d439e5a0..668952775a 100644 --- a/i18n/hi.json +++ b/i18n/hi.json @@ -441,7 +441,7 @@ "user_successfully_removed": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž {email} ⤕āĨ‹ ⤏ā¤Ģā¤˛ā¤¤ā¤žā¤ĒāĨ‚⤰āĨā¤ĩ⤕ ā¤šā¤Ÿā¤ž ā¤Ļā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆāĨ¤", "users_page_description": "ā¤ĒāĨā¤°ā¤ļā¤žā¤¸ā¤• (Admin) ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ā¤ĒāĨ‡ā¤œ", "version_check_enabled_description": "⤍⤈ ⤰ā¤ŋ⤞āĨ€ā¤œā¤ŧ ⤕āĨ€ ā¤œā¤žā¤ā¤š ⤕āĨ‡ ⤞ā¤ŋā¤ GitHub ā¤Ē⤰ ⤆ā¤ĩ⤧ā¤ŋ⤕ ⤅⤍āĨā¤°āĨ‹ā¤§ ⤏⤕āĨā¤ˇā¤Ž ⤕⤰āĨ‡ā¤‚", - "version_check_implications": "⤏⤂⤏āĨā¤•⤰⤪ ā¤œā¤žā¤ā¤š ⤏āĨā¤ĩā¤ŋā¤§ā¤ž github.com ⤕āĨ‡ ā¤¸ā¤žā¤Ĩ ⤆ā¤ĩ⤧ā¤ŋ⤕ ā¤¸ā¤‚ā¤šā¤žā¤° ā¤Ē⤰ ⤍ā¤ŋ⤰āĨā¤­ā¤° ⤕⤰⤤āĨ€ ā¤šāĨˆ", + "version_check_implications": "⤏⤂⤏āĨā¤•⤰⤪ ā¤œā¤žā¤ā¤š ⤏āĨā¤ĩā¤ŋā¤§ā¤ž {server} ⤕āĨ‡ ā¤¸ā¤žā¤Ĩ ⤆ā¤ĩ⤧ā¤ŋ⤕ ā¤¸ā¤‚ā¤šā¤žā¤° ā¤Ē⤰ ⤍ā¤ŋ⤰āĨā¤­ā¤° ⤕⤰⤤āĨ€ ā¤šāĨˆ", "version_check_settings": "⤏⤂⤏āĨā¤•⤰⤪ ⤚āĨ‡ā¤•", "version_check_settings_description": "ā¤¨ā¤ ⤏⤂⤏āĨā¤•⤰⤪ ⤅⤧ā¤ŋ⤏āĨ‚ā¤šā¤¨ā¤ž ⤕āĨ‹ ⤏⤕āĨā¤ˇā¤Ž/⤅⤕āĨā¤ˇā¤Ž ⤕⤰āĨ‡ā¤‚", "video_conversion_job": "⤟āĨā¤°ā¤žā¤‚⤏⤕āĨ‹ā¤Ą ā¤ĩāĨ€ā¤Ąā¤ŋ⤝āĨ‹", @@ -891,10 +891,6 @@ "day": "ā¤Ļā¤ŋ⤍", "days": "ā¤Ļā¤ŋ⤍", "deduplicate_all": "⤏⤭āĨ€ ⤕āĨ‹ ā¤ĄāĨā¤ĒāĨā¤˛ā¤ŋ⤕āĨ‡ā¤Ÿ ⤕⤰āĨ‡ā¤‚", - "deduplication_criteria_1": "⤛ā¤ĩā¤ŋ ā¤•ā¤ž ā¤†ā¤•ā¤žā¤° ā¤Ŧā¤žā¤‡ā¤ŸāĨā¤¸ ā¤ŽāĨ‡ā¤‚", - "deduplication_criteria_2": "EXIF ā¤ĄāĨ‡ā¤Ÿā¤ž ⤕āĨ€ ⤏⤂⤖āĨā¤¯ā¤ž", - "deduplication_info": "ā¤ĄāĨā¤ĒāĨā¤˛āĨ€ā¤•āĨ‡ā¤ļ⤍ ā¤šā¤Ÿā¤žā¤¨āĨ‡ ⤕āĨ€ ā¤œā¤žā¤¨ā¤•ā¤žā¤°āĨ€", - "deduplication_info_description": "ā¤Ē⤰ā¤ŋ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ā¤•ā¤ž ⤏āĨā¤ĩā¤šā¤žā¤˛ā¤ŋ⤤ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤ĒāĨ‚⤰āĨā¤ĩ-⤚⤝⤍ ⤕⤰⤍āĨ‡ ⤔⤰ ā¤ĄāĨā¤ĒāĨā¤˛ā¤ŋ⤕āĨ‡ā¤Ÿ ⤕āĨ‹ ā¤ĨāĨ‹ā¤• ā¤ŽāĨ‡ā¤‚ ā¤šā¤Ÿā¤žā¤¨āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤, ā¤šā¤Ž ⤍ā¤ŋā¤ŽāĨā¤¨ ā¤Ē⤰ ⤧āĨā¤¯ā¤žā¤¨ ā¤ĻāĨ‡ā¤¤āĨ‡ ā¤šāĨˆā¤‚:", "delete": "ā¤šā¤Ÿā¤žā¤ā¤", "delete_action_confirmation_message": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤•ā¤ˆ ⤇⤏ ā¤†ā¤‡ā¤Ÿā¤Ž ⤕āĨ‹ ā¤šā¤Ÿā¤žā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚? ā¤¯ā¤š ā¤•ā¤žā¤°āĨā¤°ā¤ĩā¤žā¤ˆ ā¤†ā¤‡ā¤Ÿā¤Ž ⤕āĨ‹ ⤏⤰āĨā¤ĩ⤰ ⤕āĨ€ ⤟āĨā¤°āĨˆā¤ļ ā¤ŽāĨ‡ā¤‚ ⤞āĨ‡ ā¤œā¤žā¤ā¤—āĨ€ ⤔⤰ ⤏āĨā¤Ĩā¤žā¤¨āĨ€ā¤¯ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤šā¤Ÿā¤žā¤¨āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤ĒāĨā¤ˇāĨā¤Ÿā¤ŋ ā¤Žā¤žā¤‚ā¤—āĨ‡ā¤—āĨ€", "delete_action_prompt": "{count} ā¤šā¤Ÿā¤žā¤ ā¤—ā¤", diff --git a/i18n/hr.json b/i18n/hr.json index 3337235102..0ed46addf9 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -5,7 +5,7 @@ "acknowledge": "Potvrdi", "action": "Akcija", "action_common_update": "AÅžuriranje", - "action_description": "Skup radnji koje se izvrÅĄavaju nad filtriran", + "action_description": "Skup radnji koje se izvrÅĄavaju nad filtriranim stavkama", "actions": "Akcije", "active": "Aktivno", "active_count": "Aktivno:{count}", @@ -203,7 +203,10 @@ "maintenance_settings_description": "Stavi Immich u način odrÅžavanja.", "maintenance_start": "Prebaci se u način odrÅžavanja", "maintenance_start_error": "Neuspjelo pokretanje načina odrÅžavanja.", + "maintenance_upload_backup": "Prenesi bakup baze podataka", + "maintenance_upload_backup_error": "Prijenos sigurnosne kopije nesuopjeÅĄan, je li datoteka tipa .sql-.sql.gz?", "manage_concurrency": "Upravljanje IstovremenoÅĄÄ‡u", + "manage_concurrency_description": "Idi na stranicu poslova za upravljanje konkurentoÅĄÄ‡u", "manage_log_settings": "Upravljanje postavkama zapisivanje", "map_dark_style": "Tamni stil", "map_enable_description": "Omogući značajke karte", @@ -269,7 +272,7 @@ "oauth_auto_register": "Automatska registracija", "oauth_auto_register_description": "Automatski registrirajte nove korisnike nakon prijave s OAuth", "oauth_button_text": "Tekst gumba", - "oauth_client_secret_description": "Obavezno ukoliko PKCE (Proof Key for Code Exchange) nije podrÅžan od strane OAuth pruÅžatelja", + "oauth_client_secret_description": "Obaveznoya privatnog klijenta ili ukoliko PKCE (Proof Key for Code Exchange) nije podrÅžan od javnog klijenta.", "oauth_enable_description": "Prijavite se putem OAutha", "oauth_mobile_redirect_uri": "Mobilnog Preusmjeravanja URI", "oauth_mobile_redirect_uri_override": "Nadjačavanje URI-preusmjeravanja za mobilne uređaje", @@ -287,15 +290,20 @@ "oauth_storage_quota_default_description": "Kvota u GiB koja će se koristiti kada nema zahtjeva.", "oauth_timeout": "Istek vremena zahtjeva", "oauth_timeout_description": "Istek vremena zahtjeva je u milisekundama", + "ocr_job_description": "Koristi strojno učenje za prepoznavanje teksta na slikama", "password_enable_description": "Prijava s email adresom i zaporkom", "password_settings": "Prijava zaporkom", "password_settings_description": "Upravljanje postavkama za prijavu zaporkom", "paths_validated_successfully": "Sve su putanje uspjeÅĄno potvrđene", "person_cleanup_job": "ČiÅĄÄ‡enje lica", + "queue_details": "Detalji reda čekanja", + "queues": "Posloviu redu čekanja", + "queues_page_description": "Administracija redova čekanja", "quota_size_gib": "Veličina kvote (GiB)", "refreshing_all_libraries": "OsvjeÅžavanje svih biblioteka", "registration": "Registracija administratora", "registration_description": "Budući da ste prvi korisnik na sustavu, bit ćete dodijeljeni administratorsku ulogu i odgovorni ste za administrativne poslove, a dodatne korisnike kreirat ćete sami.", + "remove_failed_jobs": "Makni neuspjeÅĄne poslove", "require_password_change_on_login": "Zahtijevajte od korisnika promjenu lozinke pri prvoj prijavi", "reset_settings_to_default": "Vrati postavke na zadane", "reset_settings_to_recent_saved": "Resetirajte postavke na nedavno spremljene postavke", @@ -303,13 +311,15 @@ "search_jobs": "TraÅži zadatkeâ€Ļ", "send_welcome_email": "PoÅĄaljite email dobrodoÅĄlice", "server_external_domain_settings": "Vanjska domena", - "server_external_domain_settings_description": "Domena za javno dijeljene linkove, uključujući http(s)://", + "server_external_domain_settings_description": "Domena za vanjske poveznice", "server_public_users": "Javni korisnici", "server_public_users_description": "Svi korisnici (ime i e-poÅĄta) navedeni su prilikom dodavanja korisnika u dijeljene albume. Kada je onemogućeno, popis korisnika bit će dostupan samo korisnicima administratora.", "server_settings": "Postavke servera", "server_settings_description": "Upravljanje postavkama servera", + "server_stats_page_description": "Statistika servera za administratore", "server_welcome_message": "Poruka dobrodoÅĄlice", "server_welcome_message_description": "Poruka koja je prikazana na prijavi.", + "settings_page_description": "Administratorske postavke", "sidecar_job": "Sidecar metapodaci", "sidecar_job_description": "Otkrijte ili sinkronizirajte sidecar metapodatke iz datotečnog sustava", "slideshow_duration_description": "Broj sekundi za prikaz svake slike", @@ -401,7 +411,7 @@ "transcoding_tone_mapping": "Tonsko preslikavanje", "transcoding_tone_mapping_description": "PokuÅĄava sačuvati izgled HDR videozapisa kada se pretvori u SDR. Svaki algoritam čini različite kompromise za boju, detalje i svjetlinu. Hable čuva detalje, Mobius čuva boju, a Reinhard svjetlinu.", "transcoding_transcode_policy": "Pravila transkodiranja", - "transcoding_transcode_policy_description": "Pravila o tome kada se video treba transkodirati. HDR videozapisi uvijek će biti transkodirani (osim ako je transkodiranje onemogućeno).", + "transcoding_transcode_policy_description": "Pravila o tome kada se video treba transkodirati. HDR videozapisi i videozapisi sa formatoom piksela razlicitim od ZUV 4:2:0 uvijek će biti transkodirani (osim ako je transkodiranje onemogućeno).", "transcoding_two_pass_encoding": "Kodiranje u dva prolaza", "transcoding_two_pass_encoding_setting_description": "Transkodiranje u dva prolaza za proizvodnju bolje kodiranih videozapisa. Kada je omogućena maksimalna brzina prijenosa (potrebna za rad s H.264 i HEVC), ovaj način rada koristi raspon brzine prijenosa na temelju maksimalne brzine prijenosa i zanemaruje CRF. Za VP9, CRF se moÅže koristiti ako je maksimalna brzina prijenosa onemogućena.", "transcoding_video_codec": "Video kodek", @@ -428,8 +438,10 @@ "user_restore_scheduled_removal": "Vrati korisnika - zakazano uklanjanje {date, date, long}", "user_settings": "Korisničke postavke", "user_settings_description": "Upravljanje korisničkim postavkama", + "user_successfully_removed": "Korisnik {email} je uspjeÅĄno uklonjen.", + "users_page_description": "Administracija korisnika stranica", "version_check_enabled_description": "Omogući provjeru verzije", - "version_check_implications": "Značajka provjere verzije oslanja se na periodičnu komunikaciju s github.com", + "version_check_implications": "Značajka provjere verzije oslanja se na periodičnu komunikaciju s {server}", "version_check_settings": "Provjera verzije", "version_check_settings_description": "Omogućite/onemogućite obavijest o novoj verziji", "video_conversion_job": "Transkodiranje videozapisa", @@ -439,6 +451,9 @@ "admin_password": "Admin lozinka", "administration": "Administracija", "advanced": "Napredno", + "advanced_settings_clear_image_cache": "ObriÅĄi međuspremnik slika", + "advanced_settings_clear_image_cache_error": "NeuspjeÅĄno čiÅĄÄ‡enje međuspremnika slika", + "advanced_settings_clear_image_cache_success": "UspjeÅĄno očiÅĄÄ‡eno {size}", "advanced_settings_enable_alternate_media_filter_subtitle": "Koristite ovu opciju za filtriranje medija tijekom sinkronizacije na temelju alternativnih kriterija. PokuÅĄajte ovo samo ako imate problema s aplikacijom koja ne prepoznaje sve albume.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTALNO] Koristite alternativni filter za sinkronizaciju albuma na uređaju", "advanced_settings_log_level_title": "Razina zapisivanja: {level}", @@ -458,6 +473,7 @@ "age_months": "Dob {months, plural, one {# mjesec} other {# mjeseca}}", "age_year_months": "Dob 1 godina, {months, plural, one {# mjesec} other {# mjeseca}}", "age_years": "{years, plural, other {Dob #}}", + "album": "Album", "album_added": "Album dodan", "album_added_notification_setting_description": "Primite obavijest e-poÅĄtom kada ste dodani u dijeljeni album", "album_cover_updated": "Naslovnica albuma aÅžurirana", @@ -474,10 +490,12 @@ "album_remove_user": "Ukloni korisnika?", "album_remove_user_confirmation": "Jeste li sigurni da Åželite ukloniti {user}?", "album_search_not_found": "Nema albuma koji odgovaraju vaÅĄem pretraÅživanju", + "album_selected": "Album odabran", "album_share_no_users": "Čini se da ste podijelili ovaj album sa svim korisnicima ili nemate nijednog korisnika s kojim biste ga dijelili.", "album_summary": "SaÅžetak albuma", "album_updated": "Album aÅžuriran", "album_updated_setting_description": "Primite obavijest e-poÅĄtom kada dijeljeni album ima nove stavke", + "album_upload_assets": "Učitaj stavku s vlastitog računala i dodaj u album", "album_user_left": "NapuÅĄten {album}", "album_user_removed": "Uklonjen {user}", "album_viewer_appbar_delete_confirm": "Jeste li sigurni da Åželite izbrisati ovaj album s vaÅĄeg računa?", @@ -495,15 +513,21 @@ "albums_default_sort_order_description": "Početni redoslijed sortiranja stavki prilikom izrade novih albuma.", "albums_feature_description": "Zbirke stavki koje se mogu dijeliti s drugim korisnicima.", "albums_on_device_count": "Albumi na uređaju ({count})", + "albums_selected": "{count, plural, one {# odabrani album} other {# odabrani albumi}}", "all": "Sve", "all_albums": "Svi albumi", "all_people": "Sve osobe", + "all_photos": "Sve slike", "all_videos": "Svi videi", "allow_dark_mode": "Dozvoli tamni način", "allow_edits": "Dozvoli izmjene", "allow_public_user_to_download": "Dopusti javnom korisniku preuzimanje", "allow_public_user_to_upload": "Dopusti javnom korisniku učitavanje", + "allowed": "DopuÅĄteno", "alt_text_qr_code": "Slika QR koda", + "always_keep": "Uvijek zadrÅži", + "always_keep_photos_hint": "Oslobodi prostora će zadrÅžati sve slike na ovom uređaju.", + "always_keep_videos_hint": "Oslobodi prostora će zadrÅžati sve videe na ovom uređaju.", "anti_clockwise": "Suprotno smjeru kazaljke na satu", "api_key": "API Ključ", "api_key_description": "Ova će vrijednost biti prikazana samo jednom. Obavezno ju kopirajte prije zatvaranja prozora.", @@ -529,10 +553,12 @@ "archived_count": "{count, plural, one {Arhivirana #} few {Arhivirane #} other {Arhivirano #}}", "are_these_the_same_person": "Je li ovo ista osoba?", "are_you_sure_to_do_this": "Jeste li sigurni da to Åželite učiniti?", + "array_field_not_fully_supported": "Polja niza zahtijevaju ručno JSON editiranje", "asset_action_delete_err_read_only": "Nije moguće izbrisati stavke samo za čitanje, preskakanje", "asset_action_share_err_offline": "Nije moguće dohvatiti izvanmreÅžne stavke, preskakanje", "asset_added_to_album": "Dodano u album", "asset_adding_to_album": "Dodavanje u albumâ€Ļ", + "asset_created": "Stavka stvorena", "asset_description_updated": "Opis stavke je aÅžuriran", "asset_filename_is_offline": "Stavka {filename} je izvan mreÅže", "asset_has_unassigned_faces": "Stavka ima nedodijeljena lica", @@ -545,6 +571,9 @@ "asset_list_layout_sub_title": "Raspored", "asset_list_settings_subtitle": "Postavke izgleda MreÅže fotografija", "asset_list_settings_title": "MreÅža fotografija", + "asset_not_found_on_device_android": "Stavka nije pronađena na ovom uređaju", + "asset_not_found_on_device_ios": "Stavka nije pronađena na ovom uređaju. Ako koristite iCloud, stavka je moÅžda nedostupna zbog loÅĄe datoteke spremljena na iCloud", + "asset_not_found_on_icloud": "Stavka nije pronađena na iCloud. Stavka je moÅžda nedostupna zbog loÅĄe datoteke spremljena na iCloud", "asset_offline": "Stavka izvan mreÅže", "asset_offline_description": "Ova vanjska stavka nije pronađena na disku. Za pomoć se obratite Immich administratoru.", "asset_restored_successfully": "Stavka uspjeÅĄno obnovljena", @@ -657,6 +686,7 @@ "backup_options_page_title": "Opcije sigurnosnog kopiranja", "backup_setting_subtitle": "Upravljajte postavkama učitavanja u pozadini i prvom planu", "backup_settings_subtitle": "Upravljaj postavkama slanja", + "backup_upload_details_page_more_details": "Pritisnite za vise informacija", "backward": "Unazad", "biometric_auth_enabled": "Biometrijska autentikacija omogućena", "biometric_locked_out": "Zaključani ste iz biometrijske autentikacije", @@ -723,8 +753,21 @@ "check_corrupt_asset_backup_button": "IzvrÅĄi provjeru", "check_corrupt_asset_backup_description": "Pokrenite ovu provjeru samo putem Wi-Fi mreÅže i nakon ÅĄto su sve stavke sigurnosno kopirane. Postupak moÅže potrajati nekoliko minuta.", "check_logs": "Provjera Zapisa", + "checksum": "Kontrolni zbroj", "choose_matching_people_to_merge": "Odaberite odgovarajuće osobe za spajanje", "city": "Grad", + "cleanup_confirm_description": "Immich je pronaÅĄao {count} stavki (stvorene prije {date}) sigurno spremljene na serveru. Ukloni lokalne kopije s ovog uređaja?", + "cleanup_confirm_prompt_title": "Ukloni s ovog uređaja?", + "cleanup_deleted_assets": "Prebačeno {count} stavki u smeće uređaja", + "cleanup_deleting": "Prebacivanje u smeće...", + "cleanup_found_assets": "Pronađeno {count} sigurnosno spremljenih stavki", + "cleanup_found_assets_with_size": "Pronađeno {count} sigurnosno spremljenih stavki ({size})", + "cleanup_icloud_shared_albums_excluded": "iCloud dijeljeni albumi nisu uključeni u skeniranje", + "cleanup_no_assets_found": "Nisu pronađene stavki koje zadovoljavaju gore kriterij. Oslobodi prostor moÅže ukloniti samo stavke koje su sigurno kopirane na serveru", + "cleanup_preview_title": "Stavke za ukloniti ({count})", + "cleanup_step3_description": "Skeniraj sigurnosnu kopiju stavki koje zadovoljavaju vaÅĄ datum i zadrÅži opcije.", + "cleanup_step4_summary": "{count} stavki (stvorene prije {date}) za ukloniti s vaÅĄeg lokalnog uređaja. Slike će ostat dostupne s Immich aplikacije.", + "cleanup_trash_hint": "Kako bi u potpunosti oslobodili prostor, otvorite sistemsku aplikaciju za galeriju i očistite smeće", "clear": "Očisti", "clear_all": "Očisti sve", "clear_all_recent_searches": "IzbriÅĄi sva nedavna pretraÅživanja", @@ -736,6 +779,8 @@ "client_cert_import": "Uvezi", "client_cert_import_success_msg": "Klijentski certifikat je uvezen", "client_cert_invalid_msg": "Neispravna datoteka certifikata ili pogreÅĄna lozinka", + "client_cert_password_message": "Unesite lozinku za ovaj certifikat", + "client_cert_password_title": "Lozinka certifikata", "client_cert_remove_msg": "Klijentski certifikat je uklonjen", "client_cert_subtitle": "PodrÅžava samo PKCS12 (.p12, .pfx) format. Uvoz/uklanjanje certifikata dostupno je samo prije prijave", "client_cert_title": "SSL klijentski certifikat [EKSPERIMENTALNO]", @@ -746,6 +791,11 @@ "color": "Boja", "color_theme": "Tema boja", "command": "Naredba", + "command_palette_prompt": "Brzo nađi stranice, akcije ili naredbe", + "command_palette_to_close": "za zatvoriti", + "command_palette_to_navigate": "za pristupiti", + "command_palette_to_select": "za selektirati", + "command_palette_to_show_all": "za prikazati sve", "comment_deleted": "Komentar izbrisan", "comment_options": "Opcije komentara", "comments_and_likes": "Komentari i lajkovi", @@ -795,9 +845,12 @@ "create_link_to_share": "Izradite vezu za dijeljenje", "create_link_to_share_description": "Dopusti svakome s vezom da vidi odabrane fotografije", "create_new": "KREIRAJ NOVO", + "create_new_face": "Stvori novo lice", "create_new_person": "Stvorite novu osobu", "create_new_person_hint": "Dodijelite odabrane stavke novoj osobi", "create_new_user": "Kreiraj novog korisnika", + "create_person": "Stvori novu osobu", + "create_person_subtitle": "Dodaj ime odabranom licu kako bi stvorio i tagirao novu osobu", "create_shared_album_page_share_add_assets": "DODAJ STAVKE", "create_shared_album_page_share_select_photos": "Odaberi fotografije", "create_shared_link": "Kreiraj dijeljeni link", @@ -808,17 +861,25 @@ "created_at": "Kreirano", "creating_linked_albums": "Izradi povezane albume...", "crop": "ObreÅži", + "crop_aspect_ratio_fixed": "Popravljeno", + "crop_aspect_ratio_free": "Slobodno", + "crop_aspect_ratio_original": "Original", + "crop_aspect_ratio_square": "Kvadrat", "curated_object_page_title": "Stvari", "current_device": "Trenutačni uređaj", "current_pin_code": "Trenutni PIN kod", "current_server_address": "Trenutna adresa posluÅžitelja", - "custom_locale": "Prilagođena Lokalizacija", - "custom_locale_description": "Formatiranje datuma i brojeva na temelju jezika i regije", + "custom_date": "Specifičan datum", + "custom_locale": "Prilagođena lokalizacija", + "custom_locale_description": "Formatiranje datuma, vremena i brojeva na temelju selektiranog jezika i regije", "custom_url": "Prilagođena URL adresa", + "cutoff_date_description": "ZadrÅži slike od zadnjihâ€Ļ", + "cutoff_day": "{count, plural, one {dan} other {dana}}", + "cutoff_year": "{count, plural, one {godina} other {godine}}", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "Tamno", - "dark_theme": "Prebaci tamnu temu", + "dark_theme": "Prebaci u tamnu temu", "date": "Datum", "date_after": "Datum nakon", "date_and_time": "Datum i Vrijeme", @@ -829,10 +890,6 @@ "day": "Dan", "days": "Dani", "deduplicate_all": "Dedupliciraj Sve", - "deduplication_criteria_1": "Veličina slike u bajtovima", - "deduplication_criteria_2": "Broj EXIF podataka", - "deduplication_info": "Informacije o uklanjanju duplikata", - "deduplication_info_description": "Za automatski odabir stavki i masovno uklanjanje duplikata, uzimamo u obzir:", "delete": "IzbriÅĄi", "delete_action_confirmation_message": "Jeste li sigurni da Åželite izbrisati ovu stavku? Ova radnja će premjestiti stavku u smeće posluÅžitelja i pitati vas Åželite li ju izbrisati lokalno", "delete_action_prompt": "{count} izbrisano", @@ -868,6 +925,7 @@ "deselect_all": "PoniÅĄti odabir svih", "details": "Detalji", "direction": "Smjer", + "disable": "Onesposobi", "disabled": "Onemogućeno", "disallow_edits": "Zabrani izmjene", "discord": "Discord", @@ -893,6 +951,7 @@ "download_include_embedded_motion_videos": "Ugrađeni videozapisi", "download_include_embedded_motion_videos_description": "Uključite videozapise ugrađene u fotografije s pokretom kao zasebnu datoteku", "download_notfound": "Preuzimanje nije pronađeno", + "download_original": "Preuzmi original", "download_paused": "Preuzimanje pauzirano", "download_settings": "Preuzmi", "download_settings_description": "Upravljajte postavkama vezanim uz preuzimanje stavki", @@ -902,10 +961,11 @@ "download_waiting_to_retry": "Čeka se ponovni pokuÅĄaj", "downloading": "Preuzimanje", "downloading_asset_filename": "Preuzimanje stavke {filename}", + "downloading_from_icloud": "Preuzmi s iCloud", "downloading_media": "Preuzimanje medija", "drop_files_to_upload": "Ispustite datoteke bilo gdje za prijenos", "duplicates": "Duplikati", - "duplicates_description": "RazrijeÅĄite svaku grupu tako da naznačite koji su duplikati, ako ih ima", + "duplicates_description": "RazrijeÅĄite svaku grupu tako da naznačite koji su duplikati, ako ih ima.", "duration": "Trajanje", "edit": "Izmjena", "edit_album": "Uredi album", @@ -933,6 +993,12 @@ "editor": "Urednik", "editor_close_without_save_prompt": "Promjene neće biti spremljene", "editor_close_without_save_title": "Zatvoriti uređivač?", + "editor_confirm_reset_all_changes": "Jeste li sigurni da Åželite resetirati sve opcije?", + "editor_discard_edits_confirm": "Odbaci izmjene", + "editor_discard_edits_prompt": "Imate nesačuvane izmjene. Jeste li sigurni da ih Åželite odbaciti?", + "editor_discard_edits_title": "Odbaci izmjene?", + "editor_rotate_left": "Rotiraj 90° u suprotnom smjeru kazaljke na satu", + "editor_rotate_right": "Rotiraj 90° u smjeru kazaljke na satu", "email": "E-poÅĄta", "email_notifications": "Obavijesti putem e-maila", "empty_folder": "Ova mapa je prazna", @@ -956,6 +1022,7 @@ "error_saving_image": "PogreÅĄka: {error}", "error_tag_face_bounding_box": "PogreÅĄka pri označavanju lica – nije moguće dohvatiti koordinate granica (bounding box)", "error_title": "GreÅĄka - NeÅĄto je poÅĄlo krivo", + "error_while_navigating": "GreÅĄka prilikom navigiranja do stavki", "errors": { "cannot_navigate_next_asset": "Nije moguće prijeći na sljedeću stavku", "cannot_navigate_previous_asset": "Nije moguće prijeći na prethodnu stavku", @@ -991,6 +1058,7 @@ "failed_to_update_notification_status": "NeuspjeÅĄno aÅžuriranje statusa obavijesti", "incorrect_email_or_password": "Netočna adresa e-poÅĄte ili lozinka", "library_folder_already_exists": "Ova putanja unosa već postoji.", + "page_not_found": "Stranica nije pronađena", "paths_validation_failed": "{paths, plural, one {# putanja nije proÅĄla} other {# putanje nisu proÅĄle}} provjeru valjanosti", "profile_picture_transparent_pixels": "Profilne slike ne smiju imati prozirne piksele. Povećajte i/ili pomaknite sliku.", "quota_higher_than_disk_size": "Postavili ste kvotu veću od veličine diska", @@ -1075,6 +1143,7 @@ "unable_to_update_user": "Nije moguće aÅžurirati korisnika", "unable_to_upload_file": "Nije moguće učitati datoteku" }, + "errors_text": "GreÅĄke", "exclusion_pattern": "Uzorak isključenja", "exif": "Exif", "exif_bottom_sheet_description": "Dodaj opis...", @@ -1085,6 +1154,7 @@ "exif_bottom_sheet_people": "OSOBE", "exif_bottom_sheet_person_add_person": "Dodaj ime", "exit_slideshow": "Izađi iz projekcije slideova", + "expand": "ProÅĄiri", "expand_all": "ProÅĄiri sve", "experimental_settings_new_asset_list_subtitle": "Rad u tijeku", "experimental_settings_new_asset_list_title": "Omogući eksperimentalnu mreÅžu fotografija", @@ -1120,6 +1190,8 @@ "features_in_development": "Značajke u razvoju", "features_setting_description": "Upravljajte značajkama aplikacije", "file_name_or_extension": "Naziv ili ekstenzija datoteke", + "file_name_text": "Ime datoteke", + "file_name_with_value": "Ime datoteke: {file_name}", "file_size": "Veličina datoteke", "filename": "Naziv datoteke", "filetype": "Vrsta datoteke", diff --git a/i18n/hu.json b/i18n/hu.json index 2521d21922..c4788b9915 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -441,7 +441,7 @@ "user_successfully_removed": "{email} felhasznÃĄlÃŗ sikeresen eltÃĄvolítva.", "users_page_description": "Admin felhasznÃĄlÃŗk oldala", "version_check_enabled_description": "Új verziÃŗk elÊrhetősÊgÊnek ellenőrzÊse", - "version_check_implications": "Az Ãēj verziÃŗk ellenőrzÊse időszakos kommunikÃĄciÃŗt igÊnyel a github.com oldallal", + "version_check_implications": "Az Ãēj verziÃŗk ellenőrzÊse időszakos kommunikÃĄciÃŗt igÊnyel a {server} oldallal", "version_check_settings": "VerziÃŗ ellenőrzÊs", "version_check_settings_description": "Az Ãēj verziÃŗrÃŗl valÃŗ ÊrtesítÊs be- Ês kikapcsolÃĄsa", "video_conversion_job": "VideÃŗk ÁtkÃŗdolÃĄsa", @@ -866,6 +866,7 @@ "crop_aspect_ratio_fixed": "RÃļgzített", "crop_aspect_ratio_free": "Tetszőleges", "crop_aspect_ratio_original": "Eredeti", + "crop_aspect_ratio_square": "NÊgyzet", "curated_object_page_title": "Dolgok", "current_device": "Ez az eszkÃļz", "current_pin_code": "AktuÃĄlis PIN kÃŗd", @@ -880,7 +881,7 @@ "daily_title_text_date": "MMM dd (E)", "daily_title_text_date_year": "yyyy MMM dd (E)", "dark": "SÃļtÊt", - "dark_theme": "SÃļtÊt tÊma kapcsolÃĄsa", + "dark_theme": "SÃļtÊt tÊmÃĄra vÃĄltÃĄs", "date": "DÃĄtum", "date_after": "DÃĄtumtÃŗl", "date_and_time": "DÃĄtum Ês idő", @@ -891,10 +892,8 @@ "day": "Nap", "days": "Napok", "deduplicate_all": "Összes deduplikÃĄlÃĄsa", - "deduplication_criteria_1": "KÊp mÊrete bÃĄjtokban", - "deduplication_criteria_2": "EXIF adatok mennyisÊge", - "deduplication_info": "DeduplikÃĄciÃŗs infÃŗ", - "deduplication_info_description": "Az automatikus elővÃĄlogatÃĄshoz Ês a duplikÃĄtumok tÃļmeges eltÃĄvolítÃĄsÃĄhoz a kÃļvetkezőket vizsgÃĄljuk:", + "default_locale": "AlapÊrtelmezett nyelvi beÃĄllítÃĄs", + "default_locale_description": "A dÃĄtumok Ês szÃĄmok formÃĄzÃĄsa a bÃļngÊsző nyelvi beÃĄllítÃĄsai alapjÃĄn", "delete": "TÃļrlÊs", "delete_action_confirmation_message": "Biztosan tÃļrÃļlni szeretnÊd ezt az elemet? Így az elem a szerver lomtÃĄrÃĄba kerÃŧl, Ês megkÊrdezi, hogy tÃļrÃļlni szeretnÊd-e a az eszkÃļzÃļn is", "delete_action_prompt": "{count} tÃļrÃļlve", @@ -970,7 +969,7 @@ "downloading_media": "MÊdia letÃļltÊse", "drop_files_to_upload": "A feltÃļltÊshez hÃēzd bÃĄrhova a fÃĄjlokat", "duplicates": "DuplikÃĄtumok", - "duplicates_description": "JelÃļld meg a duplikÃĄtumokat (ha lÊteznek) a csoportokban", + "duplicates_description": "JelÃļld meg a duplikÃĄtumokat (ha lÊteznek) a csoportokban.", "duration": "Időtartam", "edit": "SzerkesztÊs", "edit_album": "Album mÃŗdosítÃĄsa", @@ -1387,9 +1386,11 @@ "library_page_sort_title": "Album címe", "licenses": "Licencek", "light": "VilÃĄgos", + "light_theme": "VilÃĄgos tÊmÃĄra vÃĄltÃĄs", "like": "Tetszik", "like_deleted": "ReakciÃŗ tÃļrÃļlve", "link_motion_video": "Motion videÃŗ hozzÃĄrendelÊse", + "link_to_docs": "TovÃĄbbi informÃĄciÃŗÃŠrt nÊzd meg a dokumentÃĄciÃŗt.", "link_to_oauth": "CsatlakoztatÃĄs OAuth-hoz", "linked_oauth_account": "Csatlakoztatott OAuth fiÃŗk", "list": "Lista", @@ -1651,6 +1652,7 @@ "only_favorites": "Csak kedvencek", "open": "Nyitva", "open_calendar": "NaptÃĄr megnyitÃĄsa", + "open_in_browser": "MegnyitÃĄs bÃļngÊszőben", "open_in_map_view": "MegnyitÃĄs tÊrkÊp nÊzetben", "open_in_openstreetmap": "MegnyitÃĄs OpenStreetMap-ben", "open_the_search_filters": "KeresÊsi szÅąrők megnyitÃĄsa", @@ -2393,6 +2395,7 @@ "viewer_remove_from_stack": "EltÃĄvolítÃĄs a csoportbÃŗl", "viewer_stack_use_as_main_asset": "Fő elemnek beÃĄllítÃĄs", "viewer_unstack": "Csoport megszÃŧntetÊse", + "visibility": "LÃĄthatÃŗsÃĄg", "visibility_changed": "{count, plural, other {# szemÊly}} lÃĄthatÃŗsÃĄga megvÃĄltozott", "visual": "VizuÃĄlis", "visual_builder": "VizuÃĄlis ÃļsszerakÃŗ", diff --git a/i18n/id.json b/i18n/id.json index cae842d1c6..f2d34116cb 100644 --- a/i18n/id.json +++ b/i18n/id.json @@ -441,7 +441,7 @@ "user_successfully_removed": "Pengguna {email} berhasil dihapus.", "users_page_description": "Halaman pengguna admin", "version_check_enabled_description": "Aktifkan pemeriksaan versi", - "version_check_implications": "Fitur pemeriksaan versi tergantung pada komunikasi berkala dengan github.com", + "version_check_implications": "Fitur pemeriksaan versi tergantung pada komunikasi berkala dengan {server}", "version_check_settings": "Pemeriksaan Versi", "version_check_settings_description": "Aktifkan/nonaktifkan notifikasi versi baru", "video_conversion_job": "Transkode video", @@ -849,9 +849,12 @@ "create_link_to_share": "Buat tautan untuk dibagikan", "create_link_to_share_description": "Biarkan siapa pun dengan tautan melihat foto yang dipilih", "create_new": "BUAT BARU", + "create_new_face": "Buat wajah baru", "create_new_person": "Buat orang baru", "create_new_person_hint": "Tetapkan aset yang dipilih ke orang yang baru", "create_new_user": "Buat pengguna baru", + "create_person": "Buat orang", + "create_person_subtitle": "Tambahkan nama pada wajah yang dipilih untuk membuat dan menandai orang baru", "create_shared_album_page_share_add_assets": "TAMBAHKAN ASET", "create_shared_album_page_share_select_photos": "Pilih Foto", "create_shared_link": "Buat tautan bersama", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "Diperbaiki", "crop_aspect_ratio_free": "Bebas", "crop_aspect_ratio_original": "Asli", + "crop_aspect_ratio_square": "Persegi", "curated_object_page_title": "Benda", "current_device": "Perangkat saat ini", "current_pin_code": "Kode PIN saat ini", @@ -880,7 +884,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM yyyy", "dark": "Gelap", - "dark_theme": "Nyalakan mode gelap", + "dark_theme": "Beralih ke tema gelap", "date": "Tanggal", "date_after": "Tanggal setelah", "date_and_time": "Tanggal dan Waktu", @@ -891,10 +895,8 @@ "day": "Hari", "days": "Hari", "deduplicate_all": "Hapus semua duplikat", - "deduplication_criteria_1": "Ukuran gambar dalam bita", - "deduplication_criteria_2": "Hitungan data EXIF", - "deduplication_info": "Info deduplikasi", - "deduplication_info_description": "Untuk memilih aset secara otomatis dan menghapus duplikat secara massal, kami melihat:", + "default_locale": "Bahasa Default", + "default_locale_description": "Sesuaikan format tanggal dan angka sesuai dengan pengaturan wilayah browser Anda", "delete": "Hapus", "delete_action_confirmation_message": "Yakin ingin menghapus aset ini? Tindakan ini akan memindahkan aset ke tempat sampah pada server dan akan mengkonfirmasi apakah Anda ingin menghapusnya juga secara lokal", "delete_action_prompt": "{count} item telah dihapus", @@ -970,7 +972,7 @@ "downloading_media": "Mengunduh media", "drop_files_to_upload": "Lepaskan file di mana saja untuk mengunggah", "duplicates": "Duplikat", - "duplicates_description": "Selesaikan setiap kelompok dengan menunjukkan mana, jika ada, yang merupakan duplikat", + "duplicates_description": "Selesaikan setiap kelompok dengan menunjukkan mana saja yang merupakan duplikat, jika ada.", "duration": "Durasi", "edit": "Edit", "edit_album": "Edit album", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "Judul album", "licenses": "Lisensi", "light": "Terang", + "light_theme": "Ganti ke mode terang", "like": "Suka", "like_deleted": "Suka dihapus", "link_motion_video": "Tautan video gerak", + "link_to_docs": "Untuk informasi lebih lanjut, silakan lihat dokumentasi.", "link_to_oauth": "Tautkan ke OAuth", "linked_oauth_account": "Akun OAuth tertaut", "list": "Daftar", @@ -2213,6 +2217,7 @@ "tag": "Tag", "tag_assets": "Tag aset", "tag_created": "Tag yang dibuat: {tag}", + "tag_face": "Tandai wajah", "tag_feature_description": "Menjelajahi foto dan video yang dikelompokkan berdasarkan topik tag yang logis", "tag_not_found_question": "Tidak dapat menemukan tag? Buat tag baru.", "tag_people": "Beri Tag Orang", @@ -2394,6 +2399,7 @@ "viewer_remove_from_stack": "Keluarkan dari Tumpukan", "viewer_stack_use_as_main_asset": "Gunakan sebagai aset utama", "viewer_unstack": "Lepas tumpukan", + "visibility": "Visibilitas", "visibility_changed": "Keterlihatan diubah untuk {count, plural, one {# orang} other {# orang}}", "visual": "Visual", "visual_builder": "Pembangun visual", diff --git a/i18n/is.json b/i18n/is.json index a355b71661..7d15b0fd69 100644 --- a/i18n/is.json +++ b/i18n/is.json @@ -421,7 +421,7 @@ "user_successfully_removed": "Notandi {email} hefur verið fjarlÃĻgður.", "users_page_description": "Síða stjÃŗrnunarnotanda", "version_check_enabled_description": "Virkja athugun ÃĄ ÃētgÃĄfu", - "version_check_implications": "Þessi athugun hefur lotubundin samskipti við github.com", + "version_check_implications": "Þessi athugun hefur lotubundin samskipti við {server}", "version_check_settings": "Athugun ÃētgÃĄfu", "version_check_settings_description": "Af-/virkja meldingu um nÃŊja ÃētgÃĄfu", "video_conversion_job": "UmkÃŗÃ°a myndbÃļnd", diff --git a/i18n/it.json b/i18n/it.json index 75e230654b..d51e45b37b 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -441,7 +441,7 @@ "user_successfully_removed": "L'utente {email} è stato rimosso con successo.", "users_page_description": "Pagina utenti (admin)", "version_check_enabled_description": "Abilita controllo della versione", - "version_check_implications": "La funzione di controllo della versione fa uso di una comunicazione periodica con github.com", + "version_check_implications": "La funzione di controllo della versione fa uso di una comunicazione periodica con {server}", "version_check_settings": "Controllo Versione", "version_check_settings_description": "Abilita/disabilita la notifica per nuove versioni", "video_conversion_job": "Transcodifica video", @@ -849,9 +849,12 @@ "create_link_to_share": "Crea link da condividere", "create_link_to_share_description": "Permetti a chiunque con il link di vedere le foto selezionate", "create_new": "CREA NUOVO", + "create_new_face": "Crea nuova faccia", "create_new_person": "Crea nuova persona", "create_new_person_hint": "Assegna le risorse selezionate a una nuova persona", "create_new_user": "Crea nuovo utente", + "create_person": "Crea persona", + "create_person_subtitle": "Aggiungi un nome alla faccia selezionata per creare e taggare la nuova persona", "create_shared_album_page_share_add_assets": "AGGIUNGI RISORSE", "create_shared_album_page_share_select_photos": "Seleziona foto", "create_shared_link": "Crea link condiviso", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "Fisso", "crop_aspect_ratio_free": "Libero", "crop_aspect_ratio_original": "Originale", + "crop_aspect_ratio_square": "Quadrato", "curated_object_page_title": "Oggetti", "current_device": "Dispositivo attuale", "current_pin_code": "Attuale codice PIN", @@ -891,10 +895,8 @@ "day": "Giorno", "days": "Giorni", "deduplicate_all": "Elimina tutti i doppioni", - "deduplication_criteria_1": "Dimensione immagine in bytes", - "deduplication_criteria_2": "Numero di dati EXIF", - "deduplication_info": "Informazioni di deduplicazione", - "deduplication_info_description": "Per preselezionare automaticamente le risorse e rimuovere i duplicati in massa, verifichiamo:", + "default_locale": "Predefinito Locale", + "default_locale_description": "Formatta le date e i numeri sulla base del tuo browser locale", "delete": "Elimina", "delete_action_confirmation_message": "Vuoi davvero eliminare questa risorsa? Questa azione sposterà la risorsa nel cestino del server e ti chiederà se desideri eliminarla dal dispositivo", "delete_action_prompt": "{count} elementi eliminati", @@ -970,7 +972,7 @@ "downloading_media": "Scaricamento file multimediali", "drop_files_to_upload": "Rilascia i file ovunque per caricarli", "duplicates": "Duplicati", - "duplicates_description": "Risolvi ciascun gruppo indicando quali sono, se esistono, i duplicati", + "duplicates_description": "Risolvi ciascun gruppo indicando quali sono, se esistono, i duplicati.", "duration": "Durata", "edit": "Modifica", "edit_album": "Modifica album", @@ -1007,6 +1009,8 @@ "editor_edits_applied_success": "Modifiche applicate con successo", "editor_flip_horizontal": "Capovolgi in orizzontale", "editor_flip_vertical": "Capovolgi in verticale", + "editor_handle_corner": "angolo {corner, select, top_left {Alto a sinistra} top_right {Alto a destra} bottom_left {Basso a sinistra} bottom_right {Basso a destra} other {A}}", + "editor_handle_edge": "bordo {edge, select, top {Alto} bottom {Basso} left {Sinistro} right {Destro} other {Altro}}", "editor_orientation": "Orientamento", "editor_reset_all_changes": "Annulla modifiche", "editor_rotate_left": "Ruota di 90° antiorario", @@ -1385,9 +1389,11 @@ "library_page_sort_title": "Titolo album", "licenses": "Licenze", "light": "Chiaro", + "light_theme": "Cambia a tema chiaro", "like": "Mi piace", "like_deleted": "Mi piace rimosso", "link_motion_video": "Collega video in movimento", + "link_to_docs": "Per maggiori informazioni, riferirsi al documentazione.", "link_to_oauth": "Collegamento a OAuth", "linked_oauth_account": "Account OAuth collegato", "list": "Lista", @@ -2211,6 +2217,7 @@ "tag": "Tag", "tag_assets": "Tagga risorse", "tag_created": "Tag creato: {tag}", + "tag_face": "Tagga la faccia", "tag_feature_description": "Navigazione foto e video raggruppati per argomenti tag logici", "tag_not_found_question": "Non riesci a trovare un tag? Creane uno nuovo.", "tag_people": "Tagga persone", @@ -2392,6 +2399,7 @@ "viewer_remove_from_stack": "Rimuovi dal gruppo", "viewer_stack_use_as_main_asset": "Usa come risorsa principale", "viewer_unstack": "Separa dal gruppo", + "visibility": "Visibilità", "visibility_changed": "Visibilità modificata per {count, plural, one {# persona} other {# persone}}", "visual": "Visuale", "visual_builder": "Costruttore di visuale", diff --git a/i18n/ja.json b/i18n/ja.json index cd98b391ef..144c52ba83 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -53,7 +53,7 @@ "authentication_settings": "čĒč¨ŧč¨­åŽš", "authentication_settings_description": "čĒč¨ŧč¨­åŽšãŽįŽĄį†īŧˆãƒ‘゚ワãƒŧド、OAuth、そぎäģ–īŧ‰", "authentication_settings_disable_all": "æœŦåŊ“ãĢすずãĻãŽãƒ­ã‚°ã‚¤ãƒŗæ–šæŗ•ã‚’į„ĄåŠšãĢしぞすかīŧŸ ãƒ­ã‚°ã‚¤ãƒŗãŒåŽŒå…¨ãĢできãĒくãĒりぞす。", - "authentication_settings_reenable": "å†ãŗæœ‰åŠšãĢするãĢは、ã‚ĩãƒŧバãƒŧã‚ŗãƒžãƒŗãƒ‰ã‚’äŊŋį”¨ã—ãĻください。", + "authentication_settings_reenable": "再åēĻæœ‰åŠšãĢするãĢは、ã‚ĩãƒŧバãƒŧã‚ŗãƒžãƒŗãƒ‰ã‚’äŊŋį”¨ã—ãĻください。", "background_task_job": "バックグナã‚Ļãƒŗãƒ‰ã‚ŋ゚ク", "backup_database": "デãƒŧã‚ŋベãƒŧ゚ぎバックã‚ĸップをäŊœæˆ", "backup_database_enable_description": "デãƒŧã‚ŋベãƒŧ゚ぎバックã‚ĸップを有劚ãĢする", @@ -62,7 +62,7 @@ "backup_onboarding_2_description": "åˆĨ々ぎデバイ゚上ぎロãƒŧã‚ĢãƒĢã‚ŗãƒ”ãƒŧã€‚ã“ã‚Œã¯ãƒĄã‚¤ãƒŗãƒ•ã‚Ąã‚¤ãƒĢやそぎロãƒŧã‚ĢãƒĢバックã‚ĸãƒƒãƒ—ãƒ•ã‚Ąã‚¤ãƒĢをåĢãŋぞす。", "backup_onboarding_3_description": "あãĒたぎすずãĻぎデãƒŧã‚ŋ(1つぎã‚Ēフã‚ĩã‚¤ãƒˆã‚ŗãƒ”ãƒŧと2つぎロãƒŧã‚ĢãƒĢã‚ŗãƒ”ãƒŧをåĢむ)ãŽã‚ŗãƒ”ãƒŧ。", "backup_onboarding_description": "デãƒŧã‚ŋäŋč­ˇãĢは、3-2-1バックã‚ĸップæˆĻį•ĨãŽåˆŠį”¨ã‚’æŽ¨åĨ¨ã—ãžã™ã€‚å†™įœŸãƒģ動į”ģデãƒŧã‚ŋとImmichぎデãƒŧã‚ŋベãƒŧ゚をあわせãĻバックã‚ĸップすることで、より厉全ãĢäŋįŽĄã§ããžã™ã€‚", - "backup_onboarding_footer": "Immichぎバックã‚ĸップãĢé–ĸã™ã‚‹æƒ…å ąã¯ã€ãƒ‰ã‚­ãƒĨãƒĄãƒŗãƒ†ãƒŧã‚ˇãƒ§ãƒŗã‚’įĸēčĒã—ãĻください。", + "backup_onboarding_footer": "Immichぎバックã‚ĸップãĢé–ĸã™ã‚‹æƒ…å ąã¯ã€ãƒ‰ã‚­ãƒĨãƒĄãƒŗãƒˆã‚’įĸēčĒã—ãĻください。", "backup_onboarding_parts_title": "3-2-1バックã‚ĸップ:", "backup_onboarding_title": "バックã‚ĸップ", "backup_settings": "デãƒŧã‚ŋベãƒŧ゚ぎバックã‚ĸãƒƒãƒ—ãŽč¨­åŽš", @@ -126,7 +126,7 @@ "library_created": "äŊœæˆã•れたナイブナãƒĒīŧš{library}", "library_deleted": "ナイブナãƒĒは削除されぞした", "library_details": "ナイブナãƒĒãŽčŠŗį´°", - "library_folder_description": "ã‚¤ãƒŗãƒãƒŧトするフりãƒĢダを指厚しãĻください、ã‚ĩブフりãƒĢダãƒŧ内をåĢむį”ģ像と動į”ģãŒã‚šã‚­ãƒŖãƒŗã•ã‚Œãžã™", + "library_folder_description": "ã‚¤ãƒŗãƒãƒŧトするフりãƒĢダを指厚しãĻください。こぎフりãƒĢダ内(ã‚ĩブフりãƒĢダをåĢむ)ぎį”ģ像と動į”ģãŒã‚šã‚­ãƒŖãƒŗã•ã‚Œãžã™ã€‚", "library_remove_exclusion_pattern_prompt": "こぎ除外パã‚ŋãƒŧãƒŗã‚’å‰Šé™¤ã—ãĻよいですかīŧŸ", "library_remove_folder_prompt": "ã“ãŽã‚¤ãƒŗãƒãƒŧトフりãƒĢãƒ€ã‚’č§Ŗé™¤ã—ãžã™ã‹īŧŸ", "library_scanning": "åŽšæœŸã‚šã‚­ãƒŖãƒŗ", @@ -150,7 +150,7 @@ "machine_learning_availability_checks_timeout": "ãƒĒクエ゚トã‚ŋイムã‚ĸã‚Ļト", "machine_learning_availability_checks_timeout_description": "å¯į”¨æ€§ãƒã‚§ãƒƒã‚¯ãŽã‚ŋイムã‚ĸã‚Ļト時間īŧˆãƒŸãƒĒį§’å˜äŊīŧ‰", "machine_learning_clip_model": "ClipãƒĸデãƒĢ", - "machine_learning_clip_model_description": "CLIP ãƒĸデãƒĢぎ名前はここãĢãƒĒ゚トされãĻいぞす。ãƒĸデãƒĢを変更した場合は、すずãĻãŽã‚¤ãƒĄãƒŧジãĢ寞しãĻ「゚マãƒŧト検į´ĸã€ã‚¸ãƒ§ãƒ–ã‚’å†åŽŸčĄŒã™ã‚‹åŋ…čĻãŒã‚ã‚Šãžã™ã€‚", + "machine_learning_clip_model_description": "ã“ãĄã‚‰ãĢ記čŧ‰ã•れãĻいるCLIPãƒĸデãƒĢãŽåį§°ã‚’æŒ‡åŽšã—ãžã™ã€‚ãƒĸデãƒĢを変更した場合は、すずãĻぎį”ģ像ãĢ寞しãĻ「゚マãƒŧト検į´ĸã€ã‚¸ãƒ§ãƒ–ã‚’å†åŽŸčĄŒã™ã‚‹åŋ…čĻãŒã‚ã‚Šãžã™ã€‚", "machine_learning_duplicate_detection": "é‡č¤‡æ¤œå‡ē", "machine_learning_duplicate_detection_enabled": "é‡č¤‡æ¤œå‡ēぎ有劚化", "machine_learning_duplicate_detection_enabled_description": "į„ĄåŠšãĢした場合でも、厌全ãĢ同一ã‚ĸã‚ģãƒƒãƒˆãŽé‡č¤‡ã¯æŽ’é™¤ã•ã‚Œãžã™ã€‚", @@ -272,7 +272,7 @@ "oauth_auto_register": "č‡Ē動į™ģ錞", "oauth_auto_register_description": "OAuthでã‚ĩã‚¤ãƒŗã‚¤ãƒŗã—ãŸã‚ã¨ã€č‡Ēå‹•įš„ãĢ新čĻãƒĻãƒŧã‚ļãƒŧをį™ģéŒ˛ã™ã‚‹", "oauth_button_text": "ボã‚ŋãƒŗãƒ†ã‚­ã‚šãƒˆ", - "oauth_client_secret_description": "OAuthプロバイダãƒŧがPKCEをã‚ĩポãƒŧトしãĻいãĒい場合はåŋ…čρ", + "oauth_client_secret_description": "抟密クナイã‚ĸãƒŗãƒˆã€ãžãŸã¯å…Ŧ開クナイã‚ĸãƒŗãƒˆã§PKCEがã‚ĩポãƒŧトされãĻいãĒい場合ãĢåŋ…須です。", "oauth_enable_description": "OAuthã§ãƒ­ã‚°ã‚¤ãƒŗ", "oauth_mobile_redirect_uri": "ãƒĸバイãƒĢᔍãƒĒダイãƒŦクトURI", "oauth_mobile_redirect_uri_override": "ãƒĸバイãƒĢᔍãƒĒダイãƒŦクトURIīŧˆä¸Šæ›¸ãīŧ‰", @@ -311,7 +311,7 @@ "search_jobs": "ジョブを検į´ĸâ€Ļ", "send_welcome_email": "ã‚ĻェãƒĢã‚Ģム ãƒĄãƒŧãƒĢ を送äŋĄã—ぞす", "server_external_domain_settings": "å¤–éƒ¨ãƒ‰ãƒĄã‚¤ãƒŗ", - "server_external_domain_settings_description": "å…Ŧé–‹å…ąæœ‰ãƒĒãƒŗã‚¯į”¨ãŽãƒ‰ãƒĄã‚¤ãƒŗīŧˆ http(s):// をåĢめるīŧ‰", + "server_external_domain_settings_description": "外部ãƒĒãƒŗã‚¯į”¨ãŽãƒ‰ãƒĄã‚¤ãƒŗ", "server_public_users": "å…Ŧ開ãƒĻãƒŧã‚ļãƒŧ", "server_public_users_description": "å…ąæœ‰ã‚ĸãƒĢバムãĢãƒĻãƒŧã‚ļãƒŧをčŋŊ加するとすずãĻぎãƒĻãƒŧã‚ļãƒŧ (åå‰ã¨ãƒĄãƒŧãƒĢã‚ĸドãƒŦ゚) がãƒĒã‚šãƒˆåŒ–ã•ã‚Œãžã™ã€‚į„ĄåŠšãĢするとãƒĻãƒŧã‚ļãƒŧãƒĒã‚šãƒˆã¯įŽĄį†č€…ãŽãŋåˆŠį”¨å¯čƒŊãĢãĒりぞす。", "server_settings": "ã‚ĩãƒŧバãƒŧč¨­åŽš", @@ -333,7 +333,7 @@ "storage_template_migration_description": "įžåœ¨ãŽ{template}をäģĨ前ãĢã‚ĸップロãƒŧドされたã‚ĸã‚ģットãĢéŠį”¨", "storage_template_migration_info": "゚トãƒŦãƒŧã‚¸ãƒ†ãƒŗãƒ—ãƒŦãƒŧトは全ãĻãŽæ‹Ąåŧĩ子を小文字ãĢå¤‰æ›ã—ãžã™ã€‚ãƒ†ãƒŗãƒ—ãƒŦãƒŧトぎ変更は新しいã‚ĸã‚ģットãĢぎãŋéŠį”¨ã•ã‚Œãžã™ã€‚ äģĨ前ãĢã‚ĸップロãƒŧドしたã‚ĸã‚ģットãĢãƒ†ãƒŗãƒ—ãƒŦãƒŧãƒˆã‚’éĄãŖãĻéŠį”¨ã™ã‚‹ãĢは、{job} ã‚’åŽŸčĄŒã—ãĻください。", "storage_template_migration_job": "゚トãƒŦãƒŧã‚¸ãƒ†ãƒŗãƒ—ãƒŦãƒŧトį§ģčĄŒã‚¸ãƒ§ãƒ–", - "storage_template_more_details": "こぎ抟čƒŊãŽčŠŗį´°ãĢついãĻは、゚トãƒŦãƒŧã‚¸ãƒ†ãƒŗãƒ—ãƒŦãƒŧトとそぎåŊąéŸŋã‚’å‚į…§ã—ãĻください", + "storage_template_more_details": "こぎ抟čƒŊãŽčŠŗį´°ãĢついãĻは、゚トãƒŦãƒŧã‚¸ãƒ†ãƒŗãƒ—ãƒŦãƒŧãƒˆãŠã‚ˆãŗããŽåŊąéŸŋäē‹é …ã‚’å‚į…§ã—ãĻください", "storage_template_onboarding_description_v2": "ã“ãŽč¨­åŽšã‚’ã‚ĒãƒŗãĢすると、ãƒĻãƒŧã‚ļãƒŧãŽåŽšįžŠã—ãŸãƒ†ãƒŗãƒ—ãƒŦãƒŧトãĢåž“ãŖãĻč‡Ēå‹•ã§ãƒ•ã‚Ąã‚¤ãƒĢãŒæ•´į†ã•ã‚Œãžã™ã€‚čŠŗã—ã„æƒ…å ąã¯ãƒ‰ã‚­ãƒĨãƒĄãƒŗãƒ†ãƒŧã‚ˇãƒ§ãƒŗã§įĸēčĒã—ãĻください。", "storage_template_path_length": "ãŠãŠã‚ˆããŽãƒ‘ã‚šé•ˇãŽåˆļ限: {length, number}/{limit, number}", "storage_template_settings": "゚トãƒŦãƒŧジ ãƒ†ãƒŗãƒ—ãƒŦãƒŧト", @@ -411,7 +411,7 @@ "transcoding_tone_mapping": "トãƒŧãƒŗãƒžãƒƒãƒ”ãƒŗã‚°", "transcoding_tone_mapping_description": "HDR動į”ģをSDRãĢ変換する際ãĢčĻ‹ãŸį›Žã‚’įļ­æŒã—ようとčŠĻãŋぞす。各ã‚ĸãƒĢゴãƒĒã‚ēãƒ ã¯ã€č‰˛ã€čŠŗį´°ã€æ˜Žã‚‹ã•ãĢ寞しãĻį•°ãĒるトãƒŦãƒŧドã‚Ēãƒ•ã‚’čĄŒã„ãžã™ã€‚Hableã¯čŠŗį´°ã‚’įļ­æŒã—、Mobiusã¯č‰˛ã‚’įļ­æŒã—、Reinhardは明るさをįļ­æŒã—ぞす。", "transcoding_transcode_policy": "ãƒˆãƒŠãƒŗã‚šã‚ŗãƒŧドポãƒĒã‚ˇãƒŧ", - "transcoding_transcode_policy_description": "動į”ģãŒãƒˆãƒŠãƒŗã‚šã‚ŗãƒŧドされるずきかをæąēめるポãƒĒã‚ˇãƒŧ。HDR動į”ģは常ãĢãƒˆãƒŠãƒŗã‚šã‚ŗãƒŧドされぞす(ãƒˆãƒŠãƒŗã‚šã‚ŗãƒŧãƒ‰ãŒį„ĄåŠšåŒ–ã•ã‚ŒãĻいる場合を除く)。", + "transcoding_transcode_policy_description": "動į”ģãŽãƒˆãƒŠãƒŗã‚šã‚ŗãƒŧドポãƒĒã‚ˇãƒŧ。HDR動į”ģã€ãŠã‚ˆãŗYUV 4:2:0äģĨ外ぎピクã‚ģãƒĢフりãƒŧマットぎ動į”ģは、常ãĢãƒˆãƒŠãƒŗã‚šã‚ŗãƒŧドされぞす。(ãƒˆãƒŠãƒŗã‚šã‚ŗãƒŧãƒ‰ãŒį„ĄåŠšãĒ場合を除く)", "transcoding_two_pass_encoding": "Two-passã‚¨ãƒŗã‚ŗãƒŧド", "transcoding_two_pass_encoding_setting_description": "äēŒã¤ãŽãƒ‘ã‚šã§ãƒˆãƒŠãƒŗã‚šã‚ŗãƒŧãƒ‰ã—ã€ã‚ˆã‚Šã‚ˆãã‚¨ãƒŗã‚ŗãƒŧドされた動į”ģã‚’į”Ÿæˆã—ãžã™ã€‚æœ€å¤§ãƒ“ãƒƒãƒˆãƒŦãƒŧトが有劚ãĢãĒãŖãĻいる場合(H.264とHEVCが動äŊœã™ã‚‹ãŸã‚ãĢåŋ…čρ)、こぎãƒĸãƒŧドは最大ビットãƒŦãƒŧトをåŸēãĢしたビットãƒŦãƒŧãƒˆãŽį¯„å›˛ã‚’äŊŋį”¨ã—ã€CRFã‚’į„ĄčĻ–ã—ãžã™ã€‚VP9ãĢついãĻは最大ビットãƒŦãƒŧãƒˆãŽį„ĄåŠšæ™‚ãĢCRFをäŊŋうことができぞす。", "transcoding_video_codec": "動į”ģã‚ŗãƒŧデック", @@ -441,7 +441,7 @@ "user_successfully_removed": "ãƒĻãƒŧã‚ļãƒŧ {email} ã¯æ­Ŗå¸¸ãĢ削除されぞした。", "users_page_description": "įŽĄį†č€…į”¨ ãƒĻãƒŧã‚ļãƒŧ ペãƒŧジ", "version_check_enabled_description": "バãƒŧã‚¸ãƒ§ãƒŗãŽįĸēčĒã‚’æœ‰åŠšãĢする", - "version_check_implications": "こぎバãƒŧã‚¸ãƒ§ãƒŗįĸēčĒæŠŸčƒŊã¯åŽšæœŸįš„ãĒgithub.comとぎ通äŋĄãĢよりぞす", + "version_check_implications": "こぎバãƒŧã‚¸ãƒ§ãƒŗįĸēčĒæŠŸčƒŊã¯åŽšæœŸįš„ãĒ{server}とぎ通äŋĄãĢよりぞす", "version_check_settings": "バãƒŧã‚¸ãƒ§ãƒŗãƒã‚§ãƒƒã‚¯", "version_check_settings_description": "新しいバãƒŧã‚¸ãƒ§ãƒŗãŽé€šįŸĨを有劚/į„ĄåŠšãĢしぞす", "video_conversion_job": "動į”ģã‚’ãƒˆãƒŠãƒŗã‚šã‚ŗãƒŧド", @@ -794,6 +794,11 @@ "color": "ã‚Ģナãƒŧ", "color_theme": "ã‚Ģナãƒŧテãƒŧマ", "command": "ã‚ŗãƒžãƒŗãƒ‰", + "command_palette_prompt": "ペãƒŧジ、ã‚ĸã‚¯ã‚ˇãƒ§ãƒŗã€ã‚ŗãƒžãƒŗãƒ‰ã‚’į´ æ—Šãæ¤œį´ĸ", + "command_palette_to_close": "閉じる", + "command_palette_to_navigate": "æąē厚", + "command_palette_to_select": "選択", + "command_palette_to_show_all": "すずãĻ襨į¤ē", "comment_deleted": "ã‚ŗãƒĄãƒŗãƒˆãŒå‰Šé™¤ã•ã‚Œãžã—ãŸ", "comment_options": "ã‚ŗãƒĄãƒŗãƒˆč¨­åŽš", "comments_and_likes": "ã‚ŗãƒĄãƒŗãƒˆã¨ã„ã„ã­", @@ -844,9 +849,12 @@ "create_link_to_share": "å…ąæœ‰ãƒĒãƒŗã‚¯ã‚’äŊœã‚‹", "create_link_to_share_description": "ãƒĒãƒŗã‚¯ã‚’įŸĨãŖãĻいるäēēå…¨å“ĄãŒé¸æŠžã—ãŸå†™įœŸã‚’é–˛čĻ§ã§ãã‚‹ã‚ˆã†ãĢãĒりぞす", "create_new": "新čĻäŊœæˆ", + "create_new_face": "æ–°ã—ã„éĄ”ã‚’äŊœæˆ", "create_new_person": "新しいäēēį‰Šã‚’äŊœæˆ", "create_new_person_hint": "é¸æŠžã—ãŸå†™įœŸ/動į”ģを新しいäēēį‰Šã¨ã—ãĻå‰˛ã‚ŠåŊ“ãĻ", "create_new_user": "新čĻãƒĻãƒŧã‚ļãƒŧぎäŊœæˆ", + "create_person": "äēēをäŊœæˆ", + "create_person_subtitle": "é¸æŠžã—ãŸéĄ”ãĢ名前をäģ˜ã‘ãĻ、新しいäēēį‰Šã‚’į™ģ錞ãƒģã‚ŋグäģ˜ã‘する", "create_shared_album_page_share_add_assets": "å†™įœŸã‚’čŋŊ加", "create_shared_album_page_share_select_photos": "å†™įœŸã‚’é¸æŠž", "create_shared_link": "å…ąæœ‰ãƒĒãƒŗã‚¯ã‚’äŊœæˆ", @@ -861,13 +869,14 @@ "crop_aspect_ratio_fixed": "å›ē厚", "crop_aspect_ratio_free": "č‡Ēį”ą", "crop_aspect_ratio_original": "ã‚ĒãƒĒジナãƒĢ", + "crop_aspect_ratio_square": "゚クエã‚ĸ", "curated_object_page_title": "čĸĢ写äŊ“", "current_device": "įžåœ¨ãŽãƒ‡ãƒã‚¤ã‚š", "current_pin_code": "įžåœ¨ãŽPINã‚ŗãƒŧド", "current_server_address": "įžåœ¨ãŽã‚ĩãƒŧバãƒŧURL", "custom_date": "ã‚Ģ゚ã‚ŋムæ—Ĩäģ˜", - "custom_locale": "ã‚Ģ゚ã‚ŋãƒ ãƒ­ã‚ąãƒŧãƒĢ", - "custom_locale_description": "言čĒžã¨åœ°åŸŸãĢåŸēãĨいãĻæ—Ĩäģ˜ã¨æ•°å€¤ã‚’フりãƒŧマットしぞす", + "custom_locale": "言čĒžã¨åœ°åŸŸãŽæ‰‹å‹•č¨­åŽš", + "custom_locale_description": "é¸æŠžã—ãŸč¨€čĒžã¨åœ°åŸŸãŽč¨­åŽšãĢåž“ãŖãĻ、æ—Ĩäģ˜ãƒģ時åˆģãƒģ数値を書åŧč¨­åŽšã—ãžã™", "custom_url": "ã‚Ģ゚ã‚ŋムURL", "cutoff_date_description": "å†™įœŸã‚’äŋæŒã™ã‚‹æœŸé–“:", "cutoff_day": "{count, plural, one {(æ—Ĩ)} other {(æ—Ĩ)}}", @@ -875,7 +884,7 @@ "daily_title_text_date": "MM DD, EE", "daily_title_text_date_year": "yyyy MM DD, EE", "dark": "ダãƒŧクãƒĸãƒŧド", - "dark_theme": "ダãƒŧクãƒĸãƒŧド切りæ›ŋえ", + "dark_theme": "ダãƒŧクãƒĸãƒŧドãĢ切りæ›ŋえ", "date": "æ—Ĩäģ˜", "date_after": "こぎæ—ĨäģĨ降", "date_and_time": "æ—Ĩäģ˜ã¨æ™‚é–“", @@ -886,10 +895,8 @@ "day": "ナイトãƒĸãƒŧド", "days": "æ—Ĩ", "deduplicate_all": "全ãĻé‡č¤‡æŽ’é™¤", - "deduplication_criteria_1": "バイト単äŊãŽį”ģ像ã‚ĩイã‚ē", - "deduplication_criteria_2": "EXIFデãƒŧã‚ŋ数", - "deduplication_info": "é‡č¤‡æŽ’é™¤æƒ…å ą", - "deduplication_info_description": "å†™įœŸ/動į”ģをč‡Ēå‹•įš„ãĢ選択しãĻé‡č¤‡ã‚’ä¸€æ‹Ŧで削除するãĢはæŦĄãŽã‚ˆã†ãĢしぞす:", + "default_locale": "デフりãƒĢãƒˆãŽč¨€čĒžã¨åœ°åŸŸ", + "default_locale_description": "ブナã‚Ļã‚ļãŽč¨€čĒžã¨åœ°åŸŸãŽč¨­åŽšãĢåŸēãĨいãĻ、æ—Ĩäģ˜ã¨æ•°å€¤ã‚’フりãƒŧマットしぞす", "delete": "削除", "delete_action_confirmation_message": "ã“ãŽé …į›Žã‚’å‰Šé™¤ã—ãžã™ã‹īŧŸãžãšã€ã“ãŽé …į›Žã¯ã‚ĩãƒŧバãƒŧä¸ŠãŽã‚´ãƒŸįŽąã¸į§ģ動されぞす。そぎ垌、あãĒたぎデバイ゚上から削除するかをæąēめãĻいただきぞす", "delete_action_prompt": "{count}é …į›Žã‚’å‰Šé™¤ã—ãžã—ãŸ", @@ -965,7 +972,7 @@ "downloading_media": "ダã‚Ļãƒŗãƒ­ãƒŧド中", "drop_files_to_upload": "ãƒ•ã‚Ąã‚¤ãƒĢをドロップしãĻã‚ĸップロãƒŧド", "duplicates": "重複", - "duplicates_description": "ã‚‚ã—ã‚ã‚Œã°ã€é‡č¤‡ã—ãĻいるグãƒĢãƒŧプをį¤ēã™ã“ã¨ã§č§Ŗæąēしぞす", + "duplicates_description": "各グãƒĢãƒŧプをįĸēčĒã—ã€é‡č¤‡ã—ãĻã„ã‚‹é …į›Žã‚’æ•´į†ã—ãĻください。", "duration": "間隔", "edit": "ᎍ集", "edit_album": "ã‚ĸãƒĢãƒãƒ ã‚’įˇ¨é›†", @@ -1002,6 +1009,8 @@ "editor_edits_applied_success": "įˇ¨é›†ãŒæ­Ŗå¸¸ãĢ反映されぞした", "editor_flip_horizontal": "æ°´åšŗæ–šå‘ãĢ反čģĸ", "editor_flip_vertical": "åž‚į›´ãĢ反čģĸ", + "editor_handle_corner": "{corner, select, top_left {åˇĻ上ぎ} top_right {åŗä¸ŠãŽ} bottom_left {åˇĻ下ぎ} bottom_right {åŗä¸‹ãŽ} other {}}ã‚ŗãƒŧナãƒŧãƒãƒŗãƒ‰ãƒĢ", + "editor_handle_edge": "{edge, select, top {上ぎ} bottom {下ぎ} left {åˇĻぎ} right {åŗãŽ} other {}} ã‚ĩã‚¤ãƒ‰ãƒãƒŗãƒ‰ãƒĢ", "editor_orientation": "向き", "editor_reset_all_changes": "変更をãƒĒã‚ģット", "editor_rotate_left": "åæ™‚č¨ˆå›žã‚ŠãĢ90°回čģĸ", @@ -1067,6 +1076,7 @@ "failed_to_update_notification_status": "通įŸĨ゚テãƒŧã‚ŋ゚ぎ更新ãĢå¤ąæ•—ã—ãžã—ãŸ", "incorrect_email_or_password": "ãƒĄãƒŧãƒĢã‚ĸドãƒŦ゚ぞたはパ゚ワãƒŧãƒ‰ãŒé–“é•ãŖãĻいぞす", "library_folder_already_exists": "ã“ãŽã‚¤ãƒŗãƒãƒŧトパ゚はæ—ĸãĢ存在しぞす。", + "page_not_found": "ペãƒŧジがčĻ‹ã¤ã‹ã‚Šãžã›ã‚“", "paths_validation_failed": "{paths, plural, one {#個} other {#個}}ぎパ゚ぎ検č¨ŧãĢå¤ąæ•—ã—ãžã—ãŸ", "profile_picture_transparent_pixels": "ãƒ—ãƒ­ãƒ•ã‚ŖãƒŧãƒĢå†™įœŸãĢは透明ピクã‚ģãƒĢをåĢめることはできぞせん。į”ģåƒã‚’æ‹Ąå¤§/į¸Žå°ã—ãŸã‚Šį§ģ動しãĻください。", "quota_higher_than_disk_size": "ãƒ‡ã‚Ŗã‚šã‚¯åŽšé‡ã‚ˆã‚Šå¤§ãã„åŽšé‡ãŒæŒ‡åŽšã•ã‚Œãžã—ãŸ", @@ -1166,6 +1176,7 @@ "exif_bottom_sheet_people": "äēēį‰Š", "exif_bottom_sheet_person_add_person": "名前をčŋŊ加", "exit_slideshow": "ã‚šãƒŠã‚¤ãƒ‰ã‚ˇãƒ§ãƒŧをįĩ‚わる", + "expand": "åą•é–‹", "expand_all": "全ãĻåą•é–‹", "experimental_settings_new_asset_list_subtitle": "čŖŊäŊœé€”中 (WIP)", "experimental_settings_new_asset_list_title": "čŠĻé¨“įš„ãĒグãƒĒッドを有劚化", @@ -1210,6 +1221,7 @@ "filter_description": "å¯žčąĄã¨ã™ã‚‹ã‚ĸã‚ģットぎæŠŊå‡ēæĄäģļ", "filter_people": "äēēį‰Šã‚’įĩžã‚Ščžŧãŋ", "filter_places": "å ´æ‰€ã‚’ãƒ•ã‚ŖãƒĢã‚ŋãƒŧ", + "filter_tags": "ã‚ŋグでįĩžã‚Ščžŧむ", "filters": "ãƒ•ã‚ŖãƒĢã‚ŋãƒŧ", "find_them_fast": "名前で検į´ĸしãĻį´ æ—Šãį™ēčĻ‹", "first": "はじめ", @@ -1377,9 +1389,11 @@ "library_page_sort_title": "ã‚ĸãƒĢバム名", "licenses": "ナイã‚ģãƒŗã‚š", "light": "ナイトãƒĸãƒŧド", + "light_theme": "ナイトテãƒŧマãĢ切りæ›ŋえ", "like": "いいね", "like_deleted": "いいねが削除されぞした", "link_motion_video": "ãƒĸãƒŧã‚ˇãƒ§ãƒŗãƒ“ãƒ‡ã‚ĒぎãƒĒãƒŗã‚¯", + "link_to_docs": "čŠŗį´°ã¯ãƒ‰ã‚­ãƒĨãƒĄãƒŗãƒˆã‚’å‚į…§ã—ãĻください。", "link_to_oauth": "OAuthへãƒĒãƒŗã‚¯ã™ã‚‹", "linked_oauth_account": "ãƒĒãƒŗã‚¯ã•ã‚ŒãŸOAuthã‚ĸã‚Ģã‚Ļãƒŗãƒˆ", "list": "ãƒĒ゚ト", @@ -1640,6 +1654,8 @@ "online": "ã‚ĒãƒŗãƒŠã‚¤ãƒŗ", "only_favorites": "お気ãĢå…Ĩりぎãŋ", "open": "開く", + "open_calendar": "ã‚ĢãƒŦãƒŗãƒ€ãƒŧを開く", + "open_in_browser": "ブナã‚Ļã‚ļで開く", "open_in_map_view": "åœ°å›ŗčĄ¨į¤ēでčĻ‹ã‚‹", "open_in_openstreetmap": "OpenStreetMapで開く", "open_the_search_filters": "検į´ĸãƒ•ã‚ŖãƒĢã‚ŋを開く", @@ -1799,7 +1815,7 @@ "rate_asset": "é …į›Žã‚’čŠ•äžĄã™ã‚‹", "rating": "æ˜Ÿã§ãŽčŠ•äžĄ", "rating_clear": "čŠ•äžĄã‚’å–ã‚Šæļˆã™", - "rating_count": "星{count, plural, one {#つ} other {#つ}}", + "rating_count": "{count, plural, =0 {æœĒ評価} one {星#つ} other {星#つ}}", "rating_description": "æƒ…å ąæŦ„ãĢEXIFãŽčŠ•äžĄã‚’čĄ¨į¤ē", "reaction_options": "ãƒĒã‚ĸã‚¯ã‚ˇãƒ§ãƒŗãŽé¸æŠž", "read_changelog": "変更åąĨ歴をčĒ­ã‚€", @@ -1872,7 +1888,10 @@ "reset_pin_code_success": "æ­Ŗå¸¸ãĢPINã‚ŗãƒŧドをãƒĒã‚ģットしぞした", "reset_pin_code_with_password": "PINã‚ŗãƒŧドはいつでもパ゚ワãƒŧドをäŊŋãŖãĻãƒĒã‚ģットできぞす", "reset_sqlite": "SQLiteデãƒŧã‚ŋベãƒŧ゚をãƒĒã‚ģット", - "reset_sqlite_confirmation": "SQLiteをæœŦåŊ“ãĢãƒĒã‚ģットしぞすかīŧŸãƒ‡ãƒŧã‚ŋã‚’å†ãŗåŒæœŸã™ã‚‹ãŸã‚ãĢログã‚ĸã‚Ļãƒˆã—å†ãƒ­ã‚°ã‚¤ãƒŗã‚’ã™ã‚‹åŋ…čĻãŒã‚ã‚Šãžã™", + "reset_sqlite_clear_app_data": "デãƒŧã‚ŋをæļˆåŽģ", + "reset_sqlite_confirmation": "æœŦåŊ“ãĢã‚ĸプãƒĒぎデãƒŧã‚ŋをæļˆåŽģしぞすかīŧŸã™ãšãĻãŽč¨­åŽšãŒå‰Šé™¤ã•ã‚Œã€ã‚ĩã‚¤ãƒŗã‚ĸã‚Ļトされぞす。", + "reset_sqlite_confirmation_note": "æŗ¨æ„: æļˆåŽģした垌はã‚ĸプãƒĒを再čĩˇå‹•するåŋ…čĻãŒã‚ã‚Šãžã™ã€‚", + "reset_sqlite_done": "ã‚ĸプãƒĒぎデãƒŧã‚ŋをæļˆåŽģしぞした。ã‚ĸプãƒĒを再čĩˇå‹•し、もう一åēĻãƒ­ã‚°ã‚¤ãƒŗã—ãĻください。", "reset_sqlite_success": "SQLiteデãƒŧã‚ŋベãƒŧ゚ぎãƒĒã‚ģットãĢ成功しぞした", "reset_to_default": "デフりãƒĢトãĢãƒĒã‚ģット", "resolution": "č§ŖåƒåēĻ", @@ -1900,6 +1919,7 @@ "saved_settings": "č¨­åŽšã‚’äŋå­˜ã—ぞした", "say_something": "äŊ•か書きčžŧãŋぞしょう", "scaffold_body_error_occurred": "エナãƒŧがį™ēį”Ÿã—ãžã—ãŸ", + "scaffold_body_error_unrecoverable": "ä爿œŸã—ãĒいエナãƒŧがį™ēį”Ÿã—ãžã—ãŸã€‚č§Ŗæąēぎため、エナãƒŧ内厚と゚ã‚ŋックトãƒŦãƒŧ゚をDiscordぞたはGitHubã§å…ąæœ‰ã—ãĻください。指į¤ēãŒã‚ãŖãŸå ´åˆã¯ã€äģĨ下ぎボã‚ŋãƒŗã‹ã‚‰ã‚ĸプãƒĒデãƒŧã‚ŋをæļˆåŽģできぞす。", "scan": "ã‚šã‚­ãƒŖãƒŗ", "scan_all_libraries": "全ãĻぎナイブナãƒĒã‚’ã‚šã‚­ãƒŖãƒŗ", "scan_library": "ã‚šã‚­ãƒŖãƒŗ", @@ -1935,6 +1955,7 @@ "search_filter_ocr": "OCRで検į´ĸ", "search_filter_people_title": "äēēį‰Šã‚’é¸æŠž", "search_filter_star_rating": "æ˜ŸčŠ•äžĄ", + "search_filter_tags_title": "ã‚ŋグを選択", "search_for": "検į´ĸ", "search_for_existing_person": "æ—ĸ存ぎäēēį‰Šã‚’æ¤œį´ĸ", "search_no_more_result": "検į´ĸįĩæžœäģĨ上", @@ -2014,6 +2035,9 @@ "set_profile_picture": "ãƒ—ãƒ­ãƒ•ã‚ŖãƒŧãƒĢį”ģåƒã‚’č¨­åŽš", "set_slideshow_to_fullscreen": "ã‚šãƒŠã‚¤ãƒ‰ã‚ˇãƒ§ãƒŧをフãƒĢ゚クãƒĒãƒŧãƒŗãĢする", "set_stack_primary_asset": "ãƒĄã‚¤ãƒŗãŽå†™įœŸã¨ã—ãĻč¨­åŽš", + "setting_image_navigation_enable_subtitle": "有劚ãĢすると、į”ģéĸぎåˇĻįĢ¯ãžãŸã¯åŗįĢ¯ãŽ4分ぎ1ぎエãƒĒã‚ĸをã‚ŋップしãĻ、前ぎį”ģ像やæŦĄãŽį”ģ像へį§ģ動できぞす。", + "setting_image_navigation_enable_title": "ã‚ŋップ操äŊœã§į§ģ動", + "setting_image_navigation_title": "į”ģ像ぎ操äŊœ", "setting_image_viewer_help": "å†™įœŸã‚’ã‚ŋップするとã‚ĩムネイãƒĢãƒģ中į”ģčŗĒãƒģã‚ĒãƒĒジナãƒĢぎ順ãĢčĒ­ãŋčžŧãŋぞす", "setting_image_viewer_original_subtitle": "ã‚ĒãƒĒジナãƒĢぎį”ģåƒã‚’čĄ¨į¤ēしたいときãĢã‚ĒãƒŗãĢしãĻください。(最大į”ģčŗĒã§čĄ¨į¤ēされるぎで、デãƒŧã‚ŋとį̝æœĢぎ゚トãƒŦãƒŧジぎæļˆč˛ģ量がåĸ—えぞす)", "setting_image_viewer_original_title": "ã‚ĒãƒĒジナãƒĢをčĒ­ãŋčžŧむ", @@ -2180,6 +2204,7 @@ "support": "ã‚ĩポãƒŧト", "support_and_feedback": "ã‚ĩポãƒŧãƒˆã¨ãƒ•ã‚Ŗãƒŧドバック", "support_third_party_description": "ImmichãŽã‚¤ãƒŗã‚šãƒˆãƒŧãƒĢはã‚ĩãƒŧドパãƒŧãƒ†ã‚ŖãƒŧãĢã‚ˆãŖãĻãƒ‘ãƒƒã‚ąãƒŧジ化されãĻã„ãžã™ã€‚é­é‡ã—ãŸå•éĄŒã¯ããŽãƒ‘ãƒƒã‚ąãƒŧジãĢčĩˇå› ã—ãĻいる可čƒŊ性があるぎでäģĨ下ぎãƒĒãƒŗã‚¯ã‚’äŊŋãŖãĻ最初ãĢããŽãƒ‘ãƒƒã‚ąãƒŧジãĢå•éĄŒã‚’æčĩˇã—ãĻください。", + "supporter": "Supporter", "swap_merge_direction": "įĩąåˆã™ã‚‹æ–šå‘ã‚’å…Ĩれæ›ŋえ", "sync": "同期", "sync_albums": "ã‚ĸãƒĢバムを同期", @@ -2192,6 +2217,7 @@ "tag": "ã‚ŋグäģ˜ã‘する", "tag_assets": "å†™įœŸ/動į”ģãĢã‚ŋグäģ˜ã‘する", "tag_created": "ã‚ŋグ: {tag} をäŊœæˆã—ぞした", + "tag_face": "éĄ”ã‚’ã‚ŋグäģ˜ã‘", "tag_feature_description": "æ„å‘ŗã‚’æŒãŸã›ãŸã‚ŋグトでグãƒĢãƒŧプ化しãĻå†™įœŸã¨å‹•į”ģã‚’é–˛čĻ§ã™ã‚‹", "tag_not_found_question": "ã‚ŋグがčĻ‹ã¤ã‹ã‚Šãžã›ã‚“ã‹? ã“ãĄã‚‰ã‹ã‚‰ã‚ŋグをäŊœæˆã§ããžã™", "tag_people": "äēēį‰Šã‚ŋグ", @@ -2291,6 +2317,7 @@ "unstack_action_prompt": "{count}é …į›ŽãŽé‡ã­åˆã‚ã›ã‚’č§Ŗé™¤", "unstacked_assets_count": "{count, plural, one {#個} other {#個}}ãŽå†™įœŸ/動į”ģを゚ã‚ŋãƒƒã‚¯ã‹ã‚‰č§Ŗé™¤ã—ãžã—ãŸ", "unsupported_field_type": "ã‚ĩポãƒŧトされãĻいãĒã„ãƒ•ã‚ŖãƒŧãƒĢドã‚ŋイプ", + "unsupported_file_type": "ãƒ•ã‚Ąã‚¤ãƒĢåŊĸåŧã€Œ{type}」はã‚ĩポãƒŧトされãĻいãĒã„ãŸã‚ã€ãƒ•ã‚Ąã‚¤ãƒĢ「{file}」をã‚ĸップロãƒŧドできぞせん。", "untagged": "ã‚ŋã‚°ã‚’č§Ŗé™¤", "untitled_workflow": "į„ĄéĄŒãŽãƒ¯ãƒŧクフロãƒŧ", "up_next": "æŦĄã¸", @@ -2317,6 +2344,8 @@ "url": "URL", "usage": "äŊŋį”¨åŽšé‡", "use_biometric": "į”ŸäŊ“čĒč¨ŧã‚’ã”åˆŠį”¨ãã ã•ã„", + "use_browser_locale": "ブナã‚Ļã‚ļãŽč¨€čĒžã¨åœ°åŸŸãŽč¨­åŽšãĢ垓う", + "use_browser_locale_description": "ブナã‚Ļã‚ļãŽč¨€čĒžã¨åœ°åŸŸãŽč¨­åŽšãĢåž“ãŖãĻ、æ—Ĩäģ˜ãƒģ時åˆģãƒģ数値を書åŧč¨­åŽšã—ãžã™", "use_current_connection": "įžåœ¨ãŽæŽĨįƒ…å ąã‚’äŊŋᔍ", "use_custom_date_range": "äģŖã‚ã‚ŠãĢã‚Ģ゚ã‚ŋムæ—Ĩäģ˜į¯„å›˛ã‚’äŊŋᔍ", "user": "ãƒĻãƒŧã‚ļãƒŧ", @@ -2370,6 +2399,7 @@ "viewer_remove_from_stack": "゚ã‚ŋックから外す", "viewer_stack_use_as_main_asset": "ãƒĄã‚¤ãƒŗãŽį”ģ像としãĻäŊŋį”¨ã™ã‚‹", "viewer_unstack": "゚ã‚ŋãƒƒã‚¯ã‚’č§Ŗé™¤", + "visibility": "襨į¤ēč¨­åŽš", "visibility_changed": "{count, plural, one {#äēē} other {#äēē}}ぎäēēį‰ŠãŽéžčĄ¨į¤ēč¨­åŽšãŒå¤‰æ›´ã•ã‚Œãžã—ãŸ", "visual": "ビジãƒĨã‚ĸãƒĢ", "visual_builder": "ビジãƒĨã‚ĸãƒĢビãƒĢダãƒŧ", diff --git a/i18n/ka.json b/i18n/ka.json index f386a1e357..6ed5cdd6ce 100644 --- a/i18n/ka.json +++ b/i18n/ka.json @@ -2,13 +2,13 @@ "about": "შესახებ", "account": "ანგარიში", "account_settings": "ანგარიშის პარამეáƒĸრები", - "acknowledge": "მიáƒĻება", + "acknowledge": "გასაგებია", "action": "áƒĨმედება", - "action_common_update": "განაახლე", + "action_common_update": "განახლება", "action_description": "მოáƒĨმედებები გაფილáƒĸáƒ áƒŖáƒš áƒ áƒ”áƒĄáƒŖáƒ áƒĄáƒ”áƒ‘áƒ–áƒ”", "actions": "áƒĨმედებები", "active": "აáƒĨáƒĸáƒ˜áƒŖáƒ áƒ˜", - "active_count": "aáƒĨáƒĸáƒ˜áƒŖáƒ áƒ˜: {count}", + "active_count": "აáƒĨáƒĸáƒ˜áƒŖáƒ áƒ˜: {count}", "activity": "აáƒĨáƒĸივობა", "activity_changed": "აáƒĨáƒĸივობა {enabled, select, true {áƒŠáƒáƒ áƒ—áƒŖáƒšáƒ˜} other {áƒ’áƒáƒ›áƒáƒ áƒ—áƒŖáƒšáƒ˜}}", "add": "დაამაáƒĸე", @@ -35,10 +35,12 @@ "add_to_album_bottom_sheet_added": "დამაáƒĸáƒ”áƒ‘áƒŖáƒšáƒ˜áƒ {album}-ში", "add_to_album_bottom_sheet_already_exists": "{album}-ში áƒŖáƒ™áƒ•áƒ” არსებობს", "add_to_album_bottom_sheet_some_local_assets": "ზოგიერთი áƒšáƒáƒ™áƒáƒšáƒŖáƒ áƒ˜ áƒ áƒ”áƒĄáƒŖáƒ áƒĄáƒ˜ ვერ დაემაáƒĸა ალბომში", + "add_to_album_toggle": "გადართე მონიშვნა {album}_სთვის", "add_to_albums": "დაამაáƒĸე ალბომებში", "add_to_albums_count": "დაამაáƒĸე ალბომში ({count})", - "add_to_bottom_bar": "დამაáƒĸება სად", + "add_to_bottom_bar": "დაამაáƒĸე ...ში", "add_to_shared_album": "დაამაáƒĸე საზიარო ალბომში", + "add_upload_to_stack": "დაამაáƒĸე აáƒĸáƒ•áƒ˜áƒ áƒ—áƒŖáƒšáƒ˜ ქáƒĸეკში", "add_url": "დაამაáƒĸე URL", "added_to_archive": "დაარáƒĨივდა", "added_to_favorites": "დაამაáƒĸე áƒ áƒŠáƒ”áƒŖáƒšáƒ”áƒ‘áƒ¨áƒ˜", @@ -51,9 +53,15 @@ "authentication_settings_disable_all": "ნამდვილად გინდა ავáƒĸორიზაáƒĒიიქ ყველა მეთოდის გამორთვა? ავáƒĸორიზაáƒĒიაქ ვეáƒĻარანაირად შეáƒĢლებ.", "authentication_settings_reenable": "რეაáƒĨáƒĸივაáƒĒიისთვის, გამოიყენე სერვერის ბრáƒĢანება.", "background_task_job": "áƒ¤áƒáƒœáƒŖáƒ áƒ˜ დავალებები", - "backup_database": "ბაზის დამპის შეáƒĨმნა", - "backup_database_enable_description": "ბაზის დამპების ჩართვა", + "backup_database": "მონაáƒĒემთა ბაზის დამპის შეáƒĨმნა", + "backup_database_enable_description": "მონაáƒĒემთა ბაზის დამპების ჩართვა", "backup_keep_last_amount": "áƒŦინა დამპების áƒ¨áƒ”áƒĄáƒáƒœáƒáƒ áƒŠáƒŖáƒœáƒ”áƒ‘áƒ”áƒšáƒ˜ რაოდენობა", + "backup_onboarding_1_description": "გარე ასლი Cloud_ში ან სხვა áƒ¤áƒ˜áƒ–áƒ˜áƒ™áƒŖáƒ  ადგილას.", + "backup_onboarding_2_description": "áƒšáƒáƒ™áƒáƒšáƒŖáƒ áƒ˜ ასლები სხვადასხვა მოáƒŦყობილობებზე. ეს მოიáƒĒავს მთავარ ფაილებს და მთავარი ფაილების ასლებს áƒšáƒáƒ™áƒáƒšáƒŖáƒ áƒáƒ“.", + "backup_onboarding_3_description": "შენი მონაáƒĒემების მთლიანი ასლები, მათ შორის ორიგინალი ფაილები. ეს მოიáƒĒავს 1 გარე ასლს და 2 áƒšáƒáƒ™áƒáƒšáƒŖáƒ  ასლს.", + "backup_onboarding_description": " 3-2-1 სარეზერვო ქიქáƒĸემა არიქ áƒ áƒ”áƒ™áƒáƒ›áƒ”áƒœáƒ“áƒ˜áƒ áƒ”áƒ‘áƒŖáƒšáƒ˜ შენი მონაáƒĒემების დასაáƒĒავად. შენ áƒŖáƒœáƒ“áƒ შეინახო აáƒĸáƒ•áƒ˜áƒ áƒ—áƒŖáƒšáƒ˜ ფოáƒĸო/ვიდეოების და ასევე immich-იქ ბაზის ასლები ყოვლისმომáƒĒველი სარეზერვო გზისთვის.", + "backup_onboarding_footer": "მეáƒĸი ინფორმაáƒĒიისთვის immich-იქ დასარეზერვებლად , გთხოვთ მიმართეთ áƒ“áƒáƒ™áƒŖáƒ›áƒ”áƒœáƒĸაáƒĒიაქ.", + "backup_onboarding_parts_title": "3-2-1 სარეზერვო ქიქáƒĸემა მოიáƒĒავს:", "backup_onboarding_title": "მარáƒĨაფები", "backup_settings": "მონაáƒĒემთა ბაზის დამპის მორგება", "backup_settings_description": "მონაáƒĒემთა ბაზის დამპის პარამეáƒĸრების მართვა.", @@ -64,27 +72,68 @@ "confirm_email_below": "დასადასáƒĸáƒŖáƒ áƒ”áƒ‘áƒšáƒáƒ“, áƒĨვემოთ აკრიფე \"{email}\"", "confirm_reprocess_all_faces": "მართლა áƒ’áƒĄáƒŖáƒ áƒ— ყველა ქა჎იქ თავიდან áƒ“áƒáƒ›áƒŖáƒ¨áƒáƒ•áƒ”áƒ‘áƒ? ეს áƒĨმედება ხალხისათვის áƒ›áƒ˜áƒœáƒ˜áƒ­áƒ”áƒ‘áƒŖáƒš სახელებს გაáƒŦმენდს.", "confirm_user_password_reset": "ნამდვილად გინდა {user}-(ი)ქ პაროლის დარესეáƒĸება?", + "confirm_user_pin_code_reset": "დარáƒŦáƒ›áƒŖáƒœáƒ”áƒ‘áƒŖáƒšáƒ˜ ხართ, რომ áƒ’áƒĄáƒŖáƒ áƒ— {user}-იქ PIN კოდის დარესეáƒĸება?", + "copy_config_to_clipboard_description": "მიმდინარე ქიქáƒĸემის áƒ™áƒáƒœáƒ¤áƒ˜áƒ’áƒŖáƒ áƒáƒĒიიქ JSON ობიეáƒĨáƒĸიქ სახით კოპირება áƒ‘áƒŖáƒ¤áƒ”áƒ áƒ¨áƒ˜", "create_job": "შეáƒĨმენი დავალება", "cron_expression": "Cron áƒ’áƒáƒ›áƒáƒĄáƒáƒŽáƒŖáƒšáƒ”áƒ‘áƒ", + "cron_expression_description": "სკანირების ინáƒĸერვალი დააყენეთ cron ფორმაáƒĸიქ გამოყენებით. დამაáƒĸებითი ინფორმაáƒĒიისთვის იხილეთ მაგ. Crontab Guru", "disable_login": "გამორთე ავáƒĸორიზაáƒĒია", + "duplicate_detection_job_description": "მსგავსი áƒĄáƒŖáƒ áƒáƒ—áƒ”áƒ‘áƒ˜áƒĄ აáƒĻმოსაჩენად, აáƒĨáƒĸივებზე მანáƒĨáƒáƒœáƒŖáƒ áƒ˜ ქáƒŦავლების გაშვება. áƒ“áƒáƒ›áƒáƒ™áƒ˜áƒ“áƒ”áƒ‘áƒŖáƒšáƒ˜áƒ ჭკვიან áƒĢიებაზე", + "export_config_as_json_description": "ჩამოáƒĸვირთეთ მიმდინარე ქიქáƒĸემის áƒ™áƒáƒœáƒ¤áƒ˜áƒ’áƒŖáƒ áƒáƒĒია JSON ფაილის სახით", + "external_libraries_page_description": "ადმინისáƒĸრაáƒĸორის გარე ბიბლიოთეკის გვერდი", "face_detection": "ქა჎იქ ამოáƒĒნობა", + "facial_recognition_job_description": "აáƒĻმოჩენილი სახეები áƒ“áƒáƒáƒ¯áƒ’áƒŖáƒ¤áƒ”áƒ— ადამიანებად. ეს ნაბიჯი ქა჎იქ ამოáƒĒნობის áƒ“áƒáƒĄáƒ áƒŖáƒšáƒ”áƒ‘áƒ˜áƒĄ შემდეგ áƒ¨áƒ”áƒĄáƒ áƒŖáƒšáƒ“áƒ”áƒ‘áƒ. „გადაáƒĸვირთვა“ (ხელახლა) áƒáƒ¯áƒ’áƒŖáƒ¤áƒ”áƒ‘áƒĄ ყველა სახეს. â€žáƒ“áƒáƒ™áƒáƒ áƒ’áƒŖáƒšáƒ˜â€œ რიგში ათავსებს სახეებს, რომლებსაáƒĒ არ აáƒĨვთ áƒ›áƒ˜áƒœáƒ˜áƒ­áƒ”áƒ‘áƒŖáƒšáƒ˜ ადამიანი.", + "failed_job_command": "ბრáƒĢანება {command} ვერ მოხერხდა დავალების áƒ¨áƒ”áƒĄáƒáƒĄáƒ áƒŖáƒšáƒ”áƒ‘áƒšáƒáƒ“: {job}", + "force_delete_user_warning": "გაფრთხილება: ეს áƒ“áƒáƒŖáƒ§áƒáƒ•áƒœáƒ”áƒ‘áƒšáƒ˜áƒ• áƒŦაშლის მომხმარებელს და ყველა მასალას. ეს მოáƒĨმედება ვერ áƒ’áƒáƒŖáƒĨმდება და ფაილების აáƒĻდგენა áƒ¨áƒ”áƒŖáƒĢლებელია.", "image_format": "ფორმაáƒĸი", "image_format_description": "WebP ფორმაáƒĸი JPEG-ზე პაáƒĸარა ფაილებს აáƒŦარმოებს, მაგრამ მის დამზადებას áƒŖáƒ¤áƒ áƒ მეáƒĸი დრო სჭირდება.", + "image_fullsize_enabled": "ჩართე áƒĄáƒ áƒŖáƒšáƒ˜ ზომის ფოáƒĸოების გენერაáƒĒია", + "image_fullsize_enabled_description": "დააგენერირე მთლიანი ზომის ფოáƒĸოები არა ვებ áƒ›áƒ”áƒ’áƒáƒ‘áƒ áƒŖáƒšáƒ˜ ფორმაáƒĸებისთვის. როáƒĒა", + "image_fullsize_quality_description": "მთლიანი ზომის áƒĄáƒŖáƒ áƒáƒ—áƒ˜áƒĄ ჎არიქ჎ი 1-100მდეა. მეáƒĸი არიქ áƒŖáƒ™áƒ”áƒĸეთეში, მაგრამ áƒŦარმოáƒĨმნის áƒŖáƒ¤áƒ áƒ დიდ ფაილებს.", "image_fullsize_title": "áƒĄáƒ áƒŖáƒšáƒ˜ ზომის áƒ’áƒáƒ›áƒáƒĄáƒáƒŽáƒŖáƒšáƒ”áƒ‘áƒ˜áƒĄ პარამეáƒĸრები", + "image_prefer_embedded_preview": "áƒŠáƒáƒ¨áƒ”áƒœáƒ”áƒ‘áƒŖáƒšáƒ˜ გადახედვის áƒŖáƒžáƒ˜áƒ áƒáƒĸესობა", "image_prefer_wide_gamut": "áƒŖáƒžáƒ˜áƒ áƒáƒĸესობა მიენიჭოს ფერის ფართე დიაპაზონს", + "image_preview_description": "áƒĄáƒáƒ¨áƒŖáƒáƒšáƒ ზომის áƒĄáƒŖáƒ áƒáƒ—áƒ”áƒ‘áƒ˜ metadata-იქ გარეშე გამოიყენება როáƒĒა áƒœáƒáƒŽáƒŖáƒšáƒáƒ‘ 1 áƒ áƒ”áƒĄáƒĄáƒŖáƒ áƒĄ და მანáƒĨáƒáƒœáƒŖáƒ áƒ˜ ქáƒŦავლებისთვის", + "image_preview_quality_description": "გადახვედის ჎არიქ჎ი 1-100-მდე. მეáƒĸი არიქ áƒŖáƒ™áƒ”áƒ—áƒ”áƒĄáƒ˜, მაგრამ áƒŦარმოáƒĨმნის áƒŖáƒ¤áƒ áƒ დიდ ფაილს და áƒ¨áƒ”áƒŖáƒĢლია აპლიკაáƒĒიიქ შეფერხება. ნაკლები áƒĒიფრიქ დაáƒĸენებამ შეიáƒĢლება ეფეáƒĨáƒĸი იáƒĨონიოს მანáƒĨáƒáƒœáƒŖáƒ áƒ˜ ქáƒŦავლების ხარისხზე.", "image_preview_title": "áƒ’áƒáƒ›áƒáƒĄáƒáƒŽáƒŖáƒšáƒ”áƒ‘áƒ˜áƒĄ გადახედვის პარამეáƒĸრები", + "image_progressive": "áƒžáƒ áƒáƒ’áƒ áƒ”áƒĄáƒ˜áƒŖáƒšáƒ˜", + "image_progressive_description": "დააენკოდრი JPEG áƒĄáƒŖáƒ áƒáƒ—áƒ”áƒ‘áƒ˜ მიყოლებით ნელ-ნელი ჩათვირთვის ეკრანისთვის. ეს არ ეხება WebP áƒĄáƒŖáƒ áƒáƒ—áƒ”áƒ‘áƒĄ.", "image_quality": "჎არიქ჎ი", "image_resolution": "გაფართოება", + "image_resolution_description": "მაáƒĻალი გაფართოებას áƒ¨áƒ”áƒŖáƒĢლია შეინახოს მეáƒĸი დეáƒĸალი მაგრამ სჭირდება მეáƒĸი დრო ენკოდირებისთვის, დიდ ფაილებს áƒ¨áƒ”áƒŖáƒĢლიათ აპლიკაáƒĒიიქ შენელება.", "image_settings": "áƒ’áƒáƒ›áƒáƒĄáƒáƒŽáƒŖáƒšáƒ”áƒ‘áƒ˜áƒĄ პარამეáƒĸრები", - "image_settings_description": "áƒ’áƒ”áƒœáƒ”áƒ áƒ˜áƒ áƒ”áƒ‘áƒŖáƒšáƒ˜ ფოáƒĸოების ჎არიქ჎იქა და áƒ áƒ”áƒ–áƒáƒšáƒŖáƒĒიიქ მართვა", - "image_thumbnail_description": "მინიაáƒĸáƒŖáƒ áƒ მეáƒĸაინფორმაáƒĒიიქ გარეშე, რომელიáƒĒ ფოáƒĸოები áƒ¯áƒ’áƒŖáƒ¤áƒŖáƒ áƒáƒ“ თვალიერებისას გამოიყენება(მაგ. მთავარ თაიმლაინზე)", + "image_settings_description": "áƒ’áƒ”áƒœáƒ”áƒ áƒ˜áƒ áƒ”áƒ‘áƒŖáƒšáƒ˜ ფოáƒĸოების ჎არიქ჎იქა და გაფართოების მართვა", + "image_thumbnail_description": "პაáƒĸარა მინიაáƒĸáƒŖáƒ áƒ მეáƒĸაინფორმაáƒĒიიქ გარეშე, რომელიáƒĒ ფოáƒĸოები áƒ¯áƒ’áƒŖáƒ¤áƒŖáƒ áƒáƒ“ თვალიერებისას გამოიყენება(მაგ. მთავარ თაიმლაინზე)", "image_thumbnail_quality_description": "მინიაáƒĸáƒŖáƒ áƒ˜áƒĄ ჎არიქ჎ი 1-დან 100-მდე. დიდი რიáƒĒხვი შეესაბამება áƒŖáƒ™áƒ”áƒ—áƒ”áƒĄ ჎არიქ჎ქ, áƒ—áƒŖáƒ›áƒĒა, áƒŖáƒ¤áƒ áƒ დიდ ფაილებს და აპლიკაáƒĒიიქ შესაáƒĢლო შენელებას.", "image_thumbnail_title": "მინიაáƒĸáƒŖáƒ áƒ˜áƒĄ პარამეáƒĸრები", + "import_config_from_json_description": "დააიმპორáƒĸირე ქიქáƒĸემის áƒ™áƒáƒœáƒ¤áƒ˜áƒ’áƒŖáƒ áƒáƒĒია JSON áƒ™áƒáƒœáƒ¤áƒ˜áƒ’áƒŖáƒ áƒáƒĒიიქ ფაილის აáƒĸვირთვით", + "job_concurrency": "{job} áƒ™áƒáƒœáƒ™áƒŖáƒ áƒ”áƒœáƒĒია", + "job_created": "დავალება შეáƒĨმნილია", + "job_not_concurrency_safe": "ეს დავალება არ არიქ áƒ™áƒáƒœáƒ™áƒŖáƒ áƒ”áƒĒია-áƒŖáƒĄáƒáƒ¤áƒ áƒ—áƒŽáƒ.", + "job_settings": "დავალებების პარამეáƒĸრები", + "job_settings_description": "დავალების áƒ™áƒáƒœáƒ™áƒŖáƒ áƒ”áƒœáƒĒიიქ მენეჯმენáƒĸი", + "jobs_over_time": "დავალებები დროთა განმავლობაში", "library_created": "შეიáƒĨმნა ბიბლიოთეკა: {library}", "library_deleted": "ბიბლიოთეკა áƒŦაიშალა", + "library_details": "ბიბლიოთეკის დეáƒĸალები", + "library_folder_description": "დააკონკრეთე ქაáƒĨაáƒĻალდე დასაიმპორáƒĸებლად. ეს ქაáƒĨაáƒĻალდე მოიáƒĒავს áƒĨვე ქაáƒĨაáƒĻალდეებს რომლების დასკანერდება ფოáƒĸოებისთვისა და ვიდეოებისთვის.", + "library_remove_exclusion_pattern_prompt": "დარáƒŦáƒ›áƒŖáƒœáƒ”áƒ‘áƒŖáƒšáƒ˜áƒŽáƒáƒ  რომ ამ გამონაკლისი áƒœáƒ˜áƒ›áƒŖáƒ¨áƒ˜áƒĄ áƒŦაშლა გინდა?", + "library_remove_folder_prompt": "დარáƒŦáƒ›áƒŖáƒœáƒ”áƒ‘áƒŖáƒšáƒ˜ ჎არ რომ ამ იმპორáƒĸáƒ˜áƒ áƒ”áƒ‘áƒŖáƒšáƒ˜ ქაáƒĨაáƒĻალდის áƒŦაშლა გინდა?", + "library_scanning": "áƒžáƒ”áƒ áƒ˜áƒáƒ“áƒŖáƒšáƒ˜ სკანირება", + "library_scanning_description": "áƒžáƒ”áƒ áƒ˜áƒáƒ“áƒŖáƒšáƒ˜ ბიბლიოთეკის სკანირების áƒ™áƒáƒœáƒ¤áƒ˜áƒ’áƒŖáƒ áƒáƒĒია", + "library_scanning_enable_description": "ჩართე áƒžáƒ”áƒ áƒ˜áƒáƒ“áƒŖáƒšáƒ˜ ბიბლიოთეკის სკანირება", "library_settings": "გარე ბიბლიოთეკა", "library_settings_description": "გარე ბიბლიოთეკების პარამეáƒĸრების მართვა", - "logging_settings": "áƒŸáƒŖáƒ áƒœáƒáƒšáƒ˜", + "library_tasks_description": "დაასკანირე გარე ბიბლიოთეკა ახალაი და/ან შეáƒĒვლილი áƒ áƒ”áƒĄáƒŖáƒ áƒĄáƒ”áƒ‘áƒ˜áƒĄáƒ—áƒ•áƒ˜áƒĄ", + "library_updated": "áƒ’áƒáƒœáƒáƒŽáƒšáƒ”áƒ‘áƒŖáƒšáƒ˜ ბიბლიოთეკა", + "library_watching_enable_description": "დააკვირდი გარე ბიბლიოთეკას ფაილის áƒĒვლილებისთვის", + "library_watching_settings": "ბიბლიოთეკის დაკვირვება [ეáƒĨსპერიმენáƒĸáƒáƒšáƒŖáƒ áƒ˜]", + "library_watching_settings_description": "ავáƒĸომაáƒĸáƒŖáƒ áƒáƒ“ დააკვირდი შეáƒĒვლილი ფაილებისთვის", + "logging_enable_description": "áƒŸáƒŖáƒ áƒœáƒáƒšáƒ˜áƒ áƒ”áƒ‘áƒ˜áƒĄ ჩართვა", + "logging_level_description": "როáƒĒა áƒŠáƒáƒ áƒ—áƒŖáƒšáƒ˜áƒ რომელი áƒŸáƒŖáƒ áƒœáƒáƒšáƒ˜áƒ áƒ”áƒ‘áƒ˜áƒĄ დონის გამოყენება.", + "logging_settings": "áƒŸáƒŖáƒ áƒœáƒáƒšáƒ˜áƒ áƒ”áƒ‘áƒ", + "machine_learning_availability_checks_description": "ავáƒĸომაáƒĸáƒŖáƒ áƒáƒ“ აáƒĻმოაჩინე და აირჩიე áƒ—áƒáƒ•áƒ˜áƒĄáƒŖáƒ¤áƒáƒšáƒ˜ მანáƒĨáƒáƒœáƒŖáƒ áƒ˜ ქáƒŦავლების სერვერები", + "machine_learning_availability_checks_interval": "შემოáƒŦმების ინáƒĸერვალი", "machine_learning_ocr": "OCR", "map_settings": "áƒ áƒŖáƒ™áƒ", "migration_job": "მიგრაáƒĒია", diff --git a/i18n/kn.json b/i18n/kn.json index 16079d48bf..6fe20c6794 100644 --- a/i18n/kn.json +++ b/i18n/kn.json @@ -439,7 +439,7 @@ "user_successfully_removed": "ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛° {email} ➅ā˛ĩā˛°ā˛¨āŗā˛¨āŗ ➝ā˛ļā˛¸āŗā˛ĩā˛ŋā˛¯ā˛žā˛—ā˛ŋ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†.", "users_page_description": "➍ā˛ŋā˛°āŗā˛ĩā˛žā˛šā˛• ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛° ā˛Ē⺁➟", "version_check_enabled_description": "➆ā˛ĩāŗƒā˛¤āŗā˛¤ā˛ŋ ā˛Ē➰ā˛ŋā˛ļāŗ€ā˛˛ā˛¨āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", - "version_check_implications": "➆ā˛ĩāŗƒā˛¤āŗā˛¤ā˛ŋ ā˛Ē➰ā˛ŋā˛ļ⺀➞➍⺆ ā˛ĩ⺈ā˛ļā˛ŋā˛ˇāŗā˛Ÿāŗā˛¯ā˛ĩ⺁ github.com ā˛¨āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ➆ā˛ĩā˛°āŗā˛¤ā˛• ➏➂ā˛ĩā˛šā˛¨ā˛ĩā˛¨āŗā˛¨āŗ ➅ā˛ĩ➞➂ā˛Ŧā˛ŋ➏ā˛ŋā˛Ļāŗ†", + "version_check_implications": "➆ā˛ĩāŗƒā˛¤āŗā˛¤ā˛ŋ ā˛Ē➰ā˛ŋā˛ļ⺀➞➍⺆ ā˛ĩ⺈ā˛ļā˛ŋā˛ˇāŗā˛Ÿāŗā˛¯ā˛ĩ⺁ {server} ā˛¨āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ➆ā˛ĩā˛°āŗā˛¤ā˛• ➏➂ā˛ĩā˛šā˛¨ā˛ĩā˛¨āŗā˛¨āŗ ➅ā˛ĩ➞➂ā˛Ŧā˛ŋ➏ā˛ŋā˛Ļāŗ†", "version_check_settings": "➆ā˛ĩāŗƒā˛¤āŗā˛¤ā˛ŋ ā˛Ē➰ā˛ŋā˛ļ⺀➞➍⺆", "version_check_settings_description": "ā˛šāŗŠā˛¸ ➆ā˛ĩāŗƒā˛¤āŗā˛¤ā˛ŋ➝ ➅➧ā˛ŋā˛¸āŗ‚ā˛šā˛¨āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ/➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", "video_conversion_job": "ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ÿāŗā˛°ā˛žā˛¨āŗā˛¸āŗâ€Œā˛•āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛ŋ", @@ -519,6 +519,9 @@ "allow_edits": "➏➂ā˛Ēā˛žā˛Ļā˛¨āŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛…ā˛¨āŗā˛Žā˛¤ā˛ŋ➏ā˛ŋ", "allow_public_user_to_download": "ā˛¸ā˛žā˛°āŗā˛ĩ➜➍ā˛ŋ➕ ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°āŗ ā˛ĄāŗŒā˛¨āŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛…ā˛¨āŗā˛Žā˛¤ā˛ŋ➏ā˛ŋ", "allow_public_user_to_upload": "ā˛¸ā˛žā˛°āŗā˛ĩ➜➍ā˛ŋ➕ ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛ŋ➗⺆ ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛…ā˛¨āŗā˛Žā˛¤ā˛ŋ➏ā˛ŋ", + "allowed": "ā˛…ā˛¨āŗā˛Žā˛¤ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "alt_text_qr_code": "QR ā˛•āŗ‹ā˛Ąāŗ ➚ā˛ŋā˛¤āŗā˛°", + "always_keep": "ā˛¯ā˛žā˛ĩā˛žā˛—ā˛˛āŗ‚ ā˛‡ā˛Ÿāŗā˛Ÿāŗā˛•āŗŠā˛ŗāŗā˛ŗā˛ŋ", "always_keep_photos_hint": "ā˛¸āŗā˛Ĩā˛ŗā˛žā˛ĩā˛•ā˛žā˛ļ ā˛Žāŗā˛•āŗā˛¤ā˛—āŗŠā˛ŗā˛ŋ➏⺁ā˛ĩ⺁ā˛Ļ➰ā˛ŋ➂ā˛Ļ ➈ ā˛¸ā˛žā˛§ā˛¨ā˛Ļā˛˛āŗā˛˛ā˛ŋ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➇➰ā˛ŋā˛¸āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", "always_keep_videos_hint": "ā˛¸āŗā˛Ĩā˛ŗā˛žā˛ĩā˛•ā˛žā˛ļ ā˛Žāŗā˛•āŗā˛¤ā˛—āŗŠā˛ŗā˛ŋ➏⺁ā˛ĩ⺁ā˛Ļ➰ā˛ŋ➂ā˛Ļ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗāŗ ➈ ā˛¸ā˛žā˛§ā˛¨ā˛Ļā˛˛āŗā˛˛ā˛ŋ ➉➺ā˛ŋā˛¯āŗā˛¤āŗā˛¤ā˛ĩāŗ†.", "anti_clockwise": "➅ā˛Ēāŗā˛°ā˛Ļā˛•āŗā˛ˇā˛ŋā˛Ŗā˛žā˛•ā˛žā˛°ā˛ĩā˛žā˛—ā˛ŋ", @@ -533,6 +536,7 @@ "appears_in": "ā˛•ā˛žā˛Ŗā˛ŋ➏ā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗāŗā˛¤āŗā˛¤ā˛Ļāŗ†", "archive": "ā˛†ā˛°āŗā˛•āŗˆā˛ĩāŗ", "archive_or_unarchive_photo": "ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛ĩā˛¨āŗā˛¨āŗ ā˛†ā˛°āŗā˛•āŗˆā˛ĩāŗ ā˛Žā˛žā˛Ąā˛ŋ ➅ā˛Ĩā˛ĩā˛ž ā˛…ā˛¨āŗâ€Œā˛†ā˛°āŗā˛•āŗˆā˛ĩāŗ ā˛Žā˛žā˛Ąā˛ŋ", + "archive_page_no_archived_assets": "ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛†ā˛°āŗā˛•āŗˆā˛ĩāŗ ā˛Žā˛žā˛Ąā˛ŋā˛Ļ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗāŗ ā˛•ā˛‚ā˛Ąāŗā˛Ŧ➂ā˛Ļā˛ŋā˛˛āŗā˛˛", "archive_size_description": "ā˛ĄāŗŒā˛¨āŗâ€Œā˛˛āŗ‹ā˛Ąāŗâ€Œā˛—ā˛ŗā˛ŋā˛—ā˛žā˛—ā˛ŋ ā˛†ā˛°āŗā˛•āŗˆā˛ĩāŗ ā˛—ā˛žā˛¤āŗā˛°ā˛ĩā˛¨āŗā˛¨āŗ ā˛•ā˛žā˛¨āŗā˛Ģā˛ŋā˛—ā˛°āŗ ā˛Žā˛žā˛Ąā˛ŋ (GiB ā˛¨ā˛˛āŗā˛˛ā˛ŋ)", "are_these_the_same_person": "➇ā˛ĩ➰⺁ ➒➂ā˛Ļāŗ‡ ā˛ĩāŗā˛¯ā˛•āŗā˛¤ā˛ŋ➝⺇?", "are_you_sure_to_do_this": "➍⺀ā˛ĩ⺁ ➇ā˛Ļā˛¨āŗā˛¨āŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", @@ -569,6 +573,7 @@ "asset_viewer_settings_subtitle": "➍ā˛ŋā˛Žāŗā˛Ž ā˛—āŗā˛¯ā˛žā˛˛ā˛°ā˛ŋ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛• ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", "asset_viewer_settings_title": "ā˛†ā˛¸āŗā˛¤ā˛ŋ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛•", "assets": "ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗāŗ", + "assets_deleted_permanently": "{count} ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗ(➗➺⺁) ā˛ļā˛žā˛ļāŗā˛ĩ➤ā˛ĩā˛žā˛—ā˛ŋ ➅➺ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", "assets_deleted_permanently_from_server": "ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ {count} ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗ(➗➺⺁) ā˛ļā˛žā˛ļāŗā˛ĩ➤ā˛ĩā˛žā˛—ā˛ŋ ➅➺ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", "assets_removed_permanently_from_device": "➍ā˛ŋā˛Žāŗā˛Ž ā˛¸ā˛žā˛§ā˛¨ā˛Ļā˛ŋ➂ā˛Ļ {count} ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗ(ā˛—ā˛ŗā˛¨āŗā˛¨āŗ) ā˛ļā˛žā˛ļāŗā˛ĩ➤ā˛ĩā˛žā˛—ā˛ŋ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", "assets_restore_confirmation": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Žā˛˛āŗā˛˛ā˛ž ➅➍⺁ā˛Ēā˛¯āŗā˛•āŗā˛¤ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗā˛¸āŗā˛Ĩā˛žā˛Ēā˛ŋ➏➞⺁ ➍⺀ā˛ĩ⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž? ➍⺀ā˛ĩ⺁ ➈ ā˛•āŗā˛°ā˛ŋā˛¯āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➰ā˛Ļāŗā˛Ļāŗā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛! ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ➆ā˛Ģāŗâ€Œā˛˛āŗˆā˛¨āŗ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➈ ➰⺀➤ā˛ŋā˛¯ā˛˛āŗā˛˛ā˛ŋ ā˛Žā˛°āŗā˛¸āŗā˛Ĩā˛žā˛Ēā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛ ā˛Žā˛‚ā˛Ŧ⺁ā˛Ļā˛¨āŗā˛¨āŗ ā˛—ā˛Žā˛¨ā˛ŋ➏ā˛ŋ.", @@ -596,20 +601,44 @@ "backup_all": "ā˛Žā˛˛āŗā˛˛ā˛ĩāŗ‚", "backup_background_service_backup_failed_message": "ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†. ā˛Žā˛°āŗā˛Ēāŗā˛°ā˛¯ā˛¤āŗā˛¨ā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†â€Ļ", "backup_background_service_connection_failed_message": "ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛—āŗ† ➏➂ā˛Ēā˛°āŗā˛•ā˛ŋ➏➞⺁ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†. ā˛Žā˛°āŗā˛Ēāŗā˛°ā˛¯ā˛¤āŗā˛¨ā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†â€Ļ", + "backup_background_service_default_notification": "ā˛šāŗŠā˛¸ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ē➰ā˛ŋā˛ļ⺀➞ā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†â€Ļ", + "backup_background_service_in_progress_notification": "➍ā˛ŋā˛Žāŗā˛Ž ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†â€Ļ", + "backup_background_service_upload_failure_notification": "{filename} ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", "backup_controller_page_background_app_refresh_disabled_content": "ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Ŧ➺➏➞⺁ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗ > ā˛¸ā˛žā˛Žā˛žā˛¨āŗā˛¯ > ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ➰ā˛ŋā˛Ģāŗā˛°āŗ†ā˛ļāŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ➰ā˛ŋā˛Ģāŗā˛°āŗ†ā˛ļāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ.", + "backup_controller_page_background_app_refresh_disabled_title": "ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ➰ā˛ŋā˛Ģāŗā˛°āŗ†ā˛ļāŗ ➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "backup_controller_page_background_battery_info_link": "ā˛šāŗ‡ā˛—āŗ†ā˛‚ā˛Ļ⺁ ➍➍➗⺆ ➤⺋➰ā˛ŋ➏ā˛ŋ", "backup_controller_page_background_battery_info_message": "ā˛…ā˛¤āŗā˛¯āŗā˛¤āŗā˛¤ā˛Ž ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ➅➍⺁➭ā˛ĩā˛•āŗā˛•ā˛žā˛—ā˛ŋ, ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗâ€Œā˛—ā˛žā˛—ā˛ŋ ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ➚➟⺁ā˛ĩ➟ā˛ŋā˛•āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛Ŧ➂➧ā˛ŋ➏⺁ā˛ĩ ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛Ŧāŗā˛¯ā˛žā˛Ÿā˛°ā˛ŋ ➆ā˛Ēāŗā˛Ÿā˛ŋā˛Žāŗˆā˛¸āŗ‡ā˛ļā˛¨āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ.\n\n➇ā˛Ļ⺁ ā˛¸ā˛žā˛§ā˛¨-➍ā˛ŋā˛°āŗā˛Ļā˛ŋā˛ˇāŗā˛Ÿā˛ĩā˛žā˛—ā˛ŋ➰⺁ā˛ĩ⺁ā˛Ļ➰ā˛ŋ➂ā˛Ļ, ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ➍ā˛ŋā˛Žāŗā˛Ž ā˛¸ā˛žā˛§ā˛¨ ā˛¤ā˛¯ā˛žā˛°ā˛•ā˛°ā˛ŋ➗⺆ ā˛…ā˛—ā˛¤āŗā˛¯ā˛ĩā˛ŋ➰⺁ā˛ĩ ā˛Žā˛žā˛šā˛ŋ➤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛¨āŗ‹ā˛Ąā˛ŋ.", + "backup_controller_page_background_battery_info_ok": "➏➰ā˛ŋ", + "backup_controller_page_background_battery_info_title": "ā˛Ŧāŗā˛¯ā˛žā˛Ÿā˛°ā˛ŋ ➆ā˛Ēāŗā˛Ÿā˛ŋā˛Žāŗˆā˛¸āŗ‡ā˛ļā˛¨āŗâ€Œā˛—ā˛ŗāŗ", + "backup_controller_page_background_charging": "ā˛šā˛žā˛°āŗā˛œāŗ ā˛Žā˛žā˛Ąāŗā˛ĩā˛žā˛— ā˛Žā˛žā˛¤āŗā˛°", "backup_controller_page_background_configure_error": "ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ➏⺇ā˛ĩāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛•ā˛žā˛¨āŗā˛Ģā˛ŋā˛—ā˛°āŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", "backup_controller_page_background_delay": "ā˛šāŗŠā˛¸ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛ĩā˛ŋ➺➂ā˛Ŧ: {duration}", "backup_controller_page_background_description": "➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ➤⺆➰⺆➝ā˛Ļ⺆➝⺇ ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛šāŗŠā˛¸ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¸āŗā˛ĩā˛¯ā˛‚ā˛šā˛žā˛˛ā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ➏⺇ā˛ĩāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛†ā˛¨āŗ ā˛Žā˛žā˛Ąā˛ŋ", "backup_controller_page_background_is_off": "ā˛¸āŗā˛ĩā˛¯ā˛‚ā˛šā˛žā˛˛ā˛ŋ➤ ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ➆ā˛Ģāŗ ➆➗ā˛ŋā˛Ļāŗ†", "backup_controller_page_background_is_on": "ā˛¸āŗā˛ĩā˛¯ā˛‚ā˛šā˛žā˛˛ā˛ŋ➤ ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛†ā˛¨āŗ ➆➗ā˛ŋā˛Ļāŗ†", + "backup_controller_page_background_turn_off": "ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ➏⺇ā˛ĩāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➆ā˛Ģāŗ ā˛Žā˛žā˛Ąā˛ŋ", + "backup_controller_page_background_turn_on": "ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ➏⺇ā˛ĩāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛†ā˛¨āŗ ā˛Žā˛žā˛Ąā˛ŋ", + "backup_controller_page_background_wifi": "ā˛ĩ⺈-ā˛Ģ⺈ ā˛¨ā˛˛āŗā˛˛ā˛ŋ ā˛Žā˛žā˛¤āŗā˛°", + "backup_controller_page_backup": "ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ", "backup_controller_page_backup_sub": "ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛ŋā˛Ļ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗāŗ", + "backup_controller_page_created": "➰➚ā˛ŋā˛¸ā˛˛ā˛žā˛Ļ ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛•: {date}", "backup_controller_page_desc_backup": "➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ➤⺆➰⺆➝⺁ā˛ĩā˛žā˛— ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛—āŗ† ā˛šāŗŠā˛¸ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¸āŗā˛ĩā˛¯ā˛‚ā˛šā˛žā˛˛ā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛Žāŗā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛†ā˛¨āŗ ā˛Žā˛žā˛Ąā˛ŋ.", + "backup_controller_page_failed": "ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ† ({count})", + "backup_controller_page_filename": "ā˛Ģāŗˆā˛˛āŗ ā˛šāŗ†ā˛¸ā˛°āŗ: {filename} [{size}]", + "backup_controller_page_id": "ā˛ā˛Ąā˛ŋ: {id}", + "backup_controller_page_info": "ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Žā˛žā˛šā˛ŋ➤ā˛ŋ", + "backup_controller_page_none_selected": "ā˛¯ā˛žā˛ĩ⺁ā˛Ļā˛¨āŗā˛¨āŗ‚ ā˛†ā˛¯āŗā˛•āŗ† ā˛Žā˛žā˛Ąā˛ŋā˛˛āŗā˛˛", + "backup_controller_page_remainder": "ā˛ļ⺇➎", "backup_controller_page_remainder_sub": "ā˛†ā˛¯āŗā˛•āŗ†ā˛¯ā˛ŋ➂ā˛Ļ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ➉➺ā˛ŋā˛Ļā˛ŋ➰⺁ā˛ĩ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗāŗ", + "backup_controller_page_server_storage": "ā˛¸ā˛°āŗā˛ĩā˛°āŗ ā˛¸ā˛‚ā˛—āŗā˛°ā˛šā˛Ŗāŗ†", + "backup_controller_page_start_backup": "ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Ēāŗā˛°ā˛žā˛°ā˛‚ā˛­ā˛ŋ➏ā˛ŋ", "backup_controller_page_status_off": "ā˛¸āŗā˛ĩā˛¯ā˛‚ā˛šā˛žā˛˛ā˛ŋ➤ ā˛Žāŗā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ➆ā˛Ģāŗ ➆➗ā˛ŋā˛Ļāŗ†", "backup_controller_page_status_on": "ā˛¸āŗā˛ĩā˛¯ā˛‚ā˛šā˛žā˛˛ā˛ŋ➤ ā˛Žāŗā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛†ā˛¨āŗ ➆➗ā˛ŋā˛Ļāŗ†", "backup_controller_page_to_backup": "ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛Ŧāŗ‡ā˛•ā˛žā˛Ļ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—ā˛ŗāŗ", "backup_controller_page_total_sub": "ā˛†ā˛¯āŗā˛Ļ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—ā˛ŗā˛ŋ➂ā˛Ļ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛…ā˛¨ā˛¨āŗā˛¯ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗāŗ", + "backup_controller_page_turn_off": "ā˛Žāŗā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ➆ā˛Ģāŗ ā˛Žā˛žā˛Ąā˛ŋ", + "backup_controller_page_turn_on": "ā˛Žāŗā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛†ā˛¨āŗ ā˛Žā˛žā˛Ąā˛ŋ", + "backup_controller_page_uploading_file_info": "ā˛Ģāŗˆā˛˛āŗ ā˛Žā˛žā˛šā˛ŋ➤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", "backup_err_only_album": "➒➂ā˛Ļāŗ‡ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛āŗ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛", "backup_error_sync_failed": "➏ā˛ŋā˛‚ā˛•āŗ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†. ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Ēāŗā˛°ā˛•āŗā˛°ā˛ŋā˛¯āŗ†ā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛.", "backup_info_card_assets": "ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗāŗ", @@ -629,22 +658,58 @@ "biometric_not_available": "➈ ā˛¸ā˛žā˛§ā˛¨ā˛Ļā˛˛āŗā˛˛ā˛ŋ ā˛Ŧā˛¯āŗ‹ā˛Žāŗ†ā˛Ÿāŗā˛°ā˛ŋā˛•āŗ ā˛Ļ⺃ā˛ĸ⺀➕➰➪ ā˛˛ā˛­āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛", "birthdate_saved": "ā˛œā˛¨āŗā˛Ž ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛•ā˛ĩā˛¨āŗā˛¨āŗ ➝ā˛ļā˛¸āŗā˛ĩā˛ŋā˛¯ā˛žā˛—ā˛ŋ ➉➺ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", "birthdate_set_description": "ā˛Ģāŗ‹ā˛Ÿāŗ‹ ➤⺆➗⺆➝⺁ā˛ĩ ā˛¸ā˛Žā˛¯ā˛Ļā˛˛āŗā˛˛ā˛ŋ ➆ ā˛ĩāŗā˛¯ā˛•āŗā˛¤ā˛ŋ➝ ā˛ĩā˛¯ā˛¸āŗā˛¸ā˛¨āŗā˛¨āŗ ā˛˛āŗ†ā˛•āŗā˛•ā˛šā˛žā˛•ā˛˛āŗ ā˛œā˛¨āŗā˛Ž ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛•ā˛ĩā˛¨āŗā˛¨āŗ ā˛Ŧā˛ŗā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "blurred_background": "ā˛Žā˛¸āŗā˛•ā˛žā˛Ļ ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ†", "bugs_and_feature_requests": "ā˛Ļ⺋➎➗➺⺁ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩ⺈ā˛ļā˛ŋā˛ˇāŗā˛Ÿāŗā˛¯ ā˛ĩā˛ŋ➍➂➤ā˛ŋ➗➺⺁", "build": "➍ā˛ŋā˛°āŗā˛Žā˛žā˛Ŗ", + "build_image": "➚ā˛ŋā˛¤āŗā˛°ā˛ĩā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛Žā˛ŋ➏ā˛ŋ", "bulk_delete_duplicates_confirmation": "➍⺀ā˛ĩ⺁ {count, plural, one {# duplicate asset} other {# duplicate assets}} ā˛…ā˛¨āŗā˛¨āŗ ā˛Ŧāŗƒā˛šā˛¤āŗ ā˛Ēāŗā˛°ā˛Žā˛žā˛Ŗā˛Ļā˛˛āŗā˛˛ā˛ŋ ➅➺ā˛ŋ➏➞⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž? ➇ā˛Ļ⺁ ā˛Ēāŗā˛°ā˛¤ā˛ŋ ➗⺁➂ā˛Ēā˛ŋ➍ ➅➤ā˛ŋā˛ĻāŗŠā˛Ąāŗā˛Ą ā˛†ā˛¸āŗā˛¤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➉➺ā˛ŋ➏ā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗāŗā˛¤āŗā˛¤ā˛Ļāŗ† ā˛Žā˛¤āŗā˛¤āŗ ➇➤➰ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛¨ā˛•ā˛˛āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛ļā˛žā˛ļāŗā˛ĩ➤ā˛ĩā˛žā˛—ā˛ŋ ➅➺ā˛ŋā˛¸āŗā˛¤āŗā˛¤ā˛Ļāŗ†. ➍⺀ā˛ĩ⺁ ➈ ā˛•āŗā˛°ā˛ŋā˛¯āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➰ā˛Ļāŗā˛Ļāŗā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛!", "bulk_keep_duplicates_confirmation": "➍⺀ā˛ĩ⺁ {count, plural, one {# duplicate asset} other {# duplicate assets}} ā˛…ā˛¨āŗā˛¨āŗ ➇➰ā˛ŋ➏ā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛˛āŗ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž? ➇ā˛Ļ⺁ ā˛¯ā˛žā˛ĩ⺁ā˛Ļā˛¨āŗā˛¨āŗ‚ ➅➺ā˛ŋ➏ā˛Ļāŗ† ā˛Žā˛˛āŗā˛˛ā˛ž ➍➕➞ā˛ŋ ➗⺁➂ā˛Ēāŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ē➰ā˛ŋā˛šā˛°ā˛ŋā˛¸āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", "bulk_trash_duplicates_confirmation": "➍⺀ā˛ĩ⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ➝⺂ ā˛Ŧā˛˛āŗā˛•āŗ ā˛Ÿāŗā˛°āŗā˛¯ā˛žā˛ļāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛Ŧā˛¯ā˛¸āŗā˛¤āŗā˛¤āŗ€ā˛°ā˛ž {count, plural, one {# duplicate asset} other {# duplicate assets}}? ➇ā˛Ļ⺁ ā˛Ēāŗā˛°ā˛¤ā˛ŋ ➗⺁➂ā˛Ēā˛ŋ➍ ➅➤ā˛ŋā˛ĻāŗŠā˛Ąāŗā˛Ą ā˛†ā˛¸āŗā˛¤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➉➺ā˛ŋ➏ā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗāŗā˛¤āŗā˛¤ā˛Ļāŗ† ā˛Žā˛¤āŗā˛¤āŗ ➇➤➰ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛¨ā˛•ā˛˛āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ÿāŗā˛°āŗā˛¯ā˛žā˛ļāŗ ā˛Žā˛žā˛Ąāŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "buy": "ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ➖➰⺀ā˛Ļā˛ŋ➏ā˛ŋ", + "cache_settings_clear_cache_button": "ā˛¸ā˛‚ā˛—āŗā˛°ā˛šā˛ĩā˛¨āŗā˛¨āŗ ➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", "cache_settings_clear_cache_button_title": "➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗâ€Œā˛¨ ā˛•āŗā˛¯ā˛žā˛ļāŗ ā˛…ā˛¨āŗā˛¨āŗ ➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋā˛¸āŗā˛¤āŗā˛¤ā˛Ļāŗ†. ā˛•āŗā˛¯ā˛žā˛ļāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗā˛¨ā˛ŋā˛°āŗā˛Žā˛ŋ➏⺁ā˛ĩā˛ĩ➰⺆➗⺆ ➇ā˛Ļ⺁ ➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗâ€Œā˛¨ ā˛•ā˛žā˛°āŗā˛¯ā˛•āŗā˛ˇā˛Žā˛¤āŗ†ā˛¯ ā˛Žāŗ‡ā˛˛āŗ† ā˛—ā˛Žā˛¨ā˛žā˛°āŗā˛šā˛ĩā˛žā˛—ā˛ŋ ā˛Ē➰ā˛ŋā˛Ŗā˛žā˛Ž ā˛Ŧāŗ€ā˛°āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "cache_settings_duplicated_assets_clear_button": "➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "cache_settings_duplicated_assets_subtitle": "➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ā˛Ēā˛Ÿāŗā˛Ÿā˛ŋ ā˛Žā˛žā˛Ąā˛ŋ➰⺁ā˛ĩ ➍ā˛ŋā˛°āŗā˛˛ā˛•āŗā˛ˇā˛ŋā˛¸ā˛˛ā˛žā˛Ļ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗāŗ", + "cache_settings_duplicated_assets_title": "➍➕➞ā˛ŋ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗāŗ ({count})", + "cache_settings_statistics_album": "➞⺈ā˛Ŧāŗā˛°ā˛°ā˛ŋ ā˛Ĩ➂ā˛Ŧāŗâ€Œā˛¨āŗ‡ā˛˛āŗâ€Œā˛—ā˛ŗāŗ", + "cache_settings_statistics_full": "ā˛Ēāŗ‚ā˛°āŗā˛Ŗ ➚ā˛ŋā˛¤āŗā˛°ā˛—ā˛ŗāŗ", + "cache_settings_statistics_shared": "ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛‚ā˛Ą ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ā˛Ĩ➂ā˛Ŧāŗâ€Œā˛¨āŗ‡ā˛˛āŗâ€Œā˛—ā˛ŗāŗ", + "cache_settings_statistics_thumbnail": "ā˛Ĩ➂ā˛Ŧāŗâ€Œā˛¨āŗ‡ā˛˛āŗâ€Œā˛—ā˛ŗāŗ", + "cache_settings_statistics_title": "ā˛•āŗā˛¯ā˛žā˛ļāŗ ā˛Ŧ➺➕⺆", "cache_settings_subtitle": "ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ā˛ŽāŗŠā˛Ŧāŗˆā˛˛āŗ ➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗâ€Œā˛¨ ā˛•āŗā˛¯ā˛žā˛ļā˛ŋā˛‚ā˛—āŗ ā˛¨ā˛Ąā˛ĩ➺ā˛ŋā˛•āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛¯ā˛‚ā˛¤āŗā˛°ā˛ŋ➏ā˛ŋ", "cache_settings_tile_subtitle": "ā˛¸āŗā˛Ĩ➺⺀➝ ā˛¸ā˛‚ā˛—āŗā˛°ā˛šā˛Ŗāŗ†ā˛¯ ā˛¨ā˛Ąā˛ĩ➺ā˛ŋā˛•āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛¯ā˛‚ā˛¤āŗā˛°ā˛ŋ➏ā˛ŋ", + "cache_settings_tile_title": "ā˛¸āŗā˛Ĩ➺⺀➝ ā˛¸ā˛‚ā˛—āŗā˛°ā˛šā˛Ŗāŗ†", + "cache_settings_title": "ā˛•āŗā˛¯ā˛žā˛ļā˛ŋā˛‚ā˛—āŗ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗ", "camera": "ā˛•āŗā˛¯ā˛žā˛Žāŗ†ā˛°ā˛ž", + "camera_brand": "ā˛•āŗā˛¯ā˛žā˛Žāŗ†ā˛°ā˛ž ā˛Ŧāŗā˛°āŗā˛¯ā˛žā˛‚ā˛Ąāŗ", + "camera_model": "ā˛•āŗā˛¯ā˛žā˛Žāŗ†ā˛°ā˛ž ā˛Žā˛žā˛Ļ➰ā˛ŋ", "cancel": "➰ā˛Ļāŗā˛Ļāŗā˛Žā˛žā˛Ąā˛ŋ", + "cancel_search": "ā˛šāŗā˛Ąāŗā˛•ā˛žā˛Ÿ ➰ā˛Ļāŗā˛Ļāŗā˛Žā˛žā˛Ąā˛ŋ", + "canceled": "➰ā˛Ļāŗā˛Ļāŗā˛Žā˛žā˛Ąā˛ŋā˛Ļāŗ†", + "canceling": "➰ā˛Ļāŗā˛Ļāŗā˛—āŗŠā˛ŗā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", + "cannot_merge_people": "ā˛œā˛¨ā˛°ā˛¨āŗā˛¨āŗ ā˛ĩā˛ŋā˛˛āŗ€ā˛¨ā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛", "cannot_undo_this_action": "➍⺀ā˛ĩ⺁ ➈ ā˛•āŗā˛°ā˛ŋā˛¯āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➰ā˛Ļāŗā˛Ļāŗā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛!", "cannot_update_the_description": "ā˛ĩā˛ŋā˛ĩā˛°ā˛Ŗāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➍ā˛ĩ⺀➕➰ā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛", + "cast": "ā˛Ēā˛žā˛¤āŗā˛°ā˛ĩā˛°āŗā˛—", + "cast_description": "ā˛˛ā˛­āŗā˛¯ā˛ĩā˛ŋ➰⺁ā˛ĩ ā˛Ŧā˛ŋā˛¤āŗā˛¤ā˛°ā˛ŋ➏⺁ā˛ĩā˛ŋ➕⺆ ā˛—ā˛Žāŗā˛¯ā˛¸āŗā˛Ĩā˛žā˛¨ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛•ā˛žā˛¨āŗā˛Ģā˛ŋā˛—ā˛°āŗ ā˛Žā˛žā˛Ąā˛ŋ", + "change_date": "ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛• ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏ā˛ŋ", + "change_description": "ā˛ĩā˛ŋā˛ĩā˛°ā˛Ŗāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏ā˛ŋ", + "change_display_order": "ā˛Ēāŗā˛°ā˛Ļā˛°āŗā˛ļ➍ ā˛•āŗā˛°ā˛Žā˛ĩā˛¨āŗā˛¨āŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏ā˛ŋ", + "change_expiration_time": "ā˛Žāŗā˛•āŗā˛¤ā˛žā˛¯ ā˛¸ā˛Žā˛¯ā˛ĩā˛¨āŗā˛¨āŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏ā˛ŋ", + "change_location": "ā˛¸āŗā˛Ĩ➺ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏ā˛ŋ", + "change_name": "ā˛šāŗ†ā˛¸ā˛°āŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏ā˛ŋ", + "change_name_successfully": "ā˛šāŗ†ā˛¸ā˛°ā˛¨āŗā˛¨āŗ ➝ā˛ļā˛¸āŗā˛ĩā˛ŋā˛¯ā˛žā˛—ā˛ŋ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "change_password": "ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏ā˛ŋ", "change_password_description": "➍⺀ā˛ĩ⺁ ➏ā˛ŋā˛¸āŗā˛Ÿā˛Žāŗâ€Œā˛—āŗ† ā˛¸āŗˆā˛¨āŗ ā˛‡ā˛¨āŗ ā˛Žā˛žā˛Ąāŗā˛¤āŗā˛¤ā˛ŋ➰⺁ā˛ĩ⺁ā˛Ļ⺁ ➇ā˛Ļāŗ‡ ā˛ŽāŗŠā˛Ļ➞⺁ ➅ā˛Ĩā˛ĩā˛ž ➍ā˛ŋā˛Žāŗā˛Ž ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏➞⺁ ā˛ĩā˛ŋ➍➂➤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†. ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ➕⺆➺➗⺆ ā˛šāŗŠā˛¸ ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ.", + "change_password_form_confirm_password": "ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ļ⺃ā˛ĸ⺀➕➰ā˛ŋ➏ā˛ŋ", "change_password_form_description": "ā˛šā˛žā˛¯āŗ {name},\n\n➍⺀ā˛ĩ⺁ ā˛ŽāŗŠā˛Ļ➞ ā˛Ŧā˛žā˛°ā˛ŋ➗⺆ ➏ā˛ŋā˛¸āŗā˛Ÿā˛Žāŗâ€Œā˛—āŗ† ā˛¸āŗˆā˛¨āŗ ā˛‡ā˛¨āŗ ➆➗ā˛ŋā˛Ļāŗā˛Ļ⺀➰ā˛ŋ ➅ā˛Ĩā˛ĩā˛ž ➍ā˛ŋā˛Žāŗā˛Ž ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏➞⺁ ā˛ĩā˛ŋ➍➂➤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†. ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ➕⺆➺➗⺆ ā˛šāŗŠā˛¸ ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ.", "change_password_form_log_out": "➇➤➰ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛¸ā˛žā˛§ā˛¨ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛˛ā˛žā˛—āŗ ā˛”ā˛Ÿāŗ ā˛Žā˛žā˛Ąā˛ŋ", "change_password_form_log_out_description": "➇➤➰ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛¸ā˛žā˛§ā˛¨ā˛—ā˛ŗā˛ŋ➂ā˛Ļ ā˛˛ā˛žā˛—āŗ ā˛”ā˛Ÿāŗ ➆➗➞⺁ ā˛ļā˛ŋā˛Ģā˛žā˛°ā˛¸āŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "change_password_form_new_password": "ā˛šāŗŠā˛¸ ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ", + "change_password_form_password_mismatch": "ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗâ€Œā˛—ā˛ŗāŗ ā˛šāŗŠā˛‚ā˛Ļā˛ŋā˛•āŗ†ā˛¯ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛˛āŗā˛˛", + "change_password_form_reenter_new_password": "ā˛šāŗŠā˛¸ ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Žā˛¤āŗā˛¤āŗ† ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ", + "change_pin_code": "ā˛Ēā˛ŋā˛¨āŗ ā˛•āŗ‹ā˛Ąāŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏ā˛ŋ", + "change_trigger": "ā˛Ÿāŗā˛°ā˛ŋā˛—āŗā˛—ā˛°āŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏ā˛ŋ", "change_trigger_prompt": "➍⺀ā˛ĩ⺁ ā˛Ÿāŗā˛°ā˛ŋā˛—āŗā˛—ā˛°āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏➞⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž? ➇ā˛Ļ⺁ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛…ā˛¸āŗā˛¤ā˛ŋā˛¤āŗā˛ĩā˛Ļā˛˛āŗā˛˛ā˛ŋ➰⺁ā˛ĩ ā˛•āŗā˛°ā˛ŋ➝⺆➗➺⺁ ā˛Žā˛¤āŗā˛¤āŗ ā˛Ģā˛ŋā˛˛āŗā˛Ÿā˛°āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", "charging_requirement_mobile_backup": "ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗâ€Œā˛—āŗ† ā˛¸ā˛žā˛§ā˛¨ā˛ĩ⺁ ā˛šā˛žā˛°āŗā˛œāŗ ā˛†ā˛—āŗā˛¤āŗā˛¤ā˛ŋ➰ā˛Ŧ⺇➕⺁", "check_corrupt_asset_backup": "ā˛­āŗā˛°ā˛ˇāŗā˛Ÿ ā˛†ā˛¸āŗā˛¤ā˛ŋ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗâ€Œā˛—ā˛ŗā˛ŋā˛—ā˛žā˛—ā˛ŋ ā˛Ē➰ā˛ŋā˛ļ⺀➞ā˛ŋ➏ā˛ŋ", @@ -661,15 +726,31 @@ "cleanup_step4_summary": "➍ā˛ŋā˛Žāŗā˛Ž ā˛¸āŗā˛Ĩ➺⺀➝ ā˛¸ā˛žā˛§ā˛¨ā˛Ļā˛ŋ➂ā˛Ļ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛āŗ {count} ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ({date} ā˛•āŗā˛•ā˛ŋ➂➤ ā˛ŽāŗŠā˛Ļ➞⺁ ➰➚ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†). ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛ĩāŗ‡ā˛ļā˛ŋ➏ā˛Ŧā˛šāŗā˛Ļ⺁.", "cleanup_trash_hint": "ā˛ļāŗ‡ā˛–ā˛°ā˛Ŗā˛ž ā˛¸āŗā˛Ĩ➺ā˛ĩā˛¨āŗā˛¨āŗ ➏➂ā˛Ēāŗ‚ā˛°āŗā˛Ŗā˛ĩā˛žā˛—ā˛ŋ ā˛Žā˛°ā˛ŗā˛ŋ ā˛Ēā˛Ąāŗ†ā˛¯ā˛˛āŗ, ➏ā˛ŋā˛¸āŗā˛Ÿā˛Žāŗ ā˛—āŗā˛¯ā˛žā˛˛ā˛°ā˛ŋ ➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ➤⺆➰⺆➝ā˛ŋ➰ā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ ➕➏ā˛ĩā˛¨āŗā˛¨āŗ ā˛–ā˛žā˛˛ā˛ŋ ā˛Žā˛žā˛Ąā˛ŋ", "clear": "➍ā˛ŋā˛°āŗā˛Žā˛˛", + "clear_all": "ā˛Žā˛˛āŗā˛˛ā˛ĩā˛¨āŗā˛¨āŗ‚ ➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", "clear_all_recent_searches": "ā˛‡ā˛¤āŗā˛¤āŗ€ā˛šā˛ŋ➍ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛šāŗā˛Ąāŗā˛•ā˛žā˛Ÿā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "clear_file_cache": "ā˛Ģāŗˆā˛˛āŗ ā˛¸ā˛‚ā˛—āŗā˛°ā˛šā˛ĩā˛¨āŗā˛¨āŗ ➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "clear_message": "➏➂ā˛Ļāŗ‡ā˛ļā˛ĩā˛¨āŗā˛¨āŗ ➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "clear_value": "ā˛ŽāŗŒā˛˛āŗā˛¯ā˛ĩā˛¨āŗā˛¨āŗ ➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "client_cert_dialog_msg_confirm": "➏➰ā˛ŋ", + "client_cert_enter_password": "ā˛Ēā˛žā˛¸āŗā˛ĩā˛°āŗā˛Ąāŗ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ", + "client_cert_import": "ā˛†ā˛Žā˛Ļ⺁ ā˛Žā˛žā˛Ąā˛ŋ", + "client_cert_import_success_msg": "ā˛•āŗā˛˛āŗˆā˛‚ā˛Ÿāŗ ā˛Ēāŗā˛°ā˛Žā˛žā˛Ŗā˛Ēā˛¤āŗā˛°ā˛ĩā˛¨āŗā˛¨āŗ ā˛†ā˛Žā˛Ļ⺁ ā˛Žā˛žā˛Ąā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", "client_cert_invalid_msg": "ā˛…ā˛Žā˛žā˛¨āŗā˛¯ ā˛Ēāŗā˛°ā˛Žā˛žā˛Ŗā˛Ēā˛¤āŗā˛° ā˛Ģāŗˆā˛˛āŗ ➅ā˛Ĩā˛ĩā˛ž ➤ā˛Ēāŗā˛Ē⺁ ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ", "client_cert_password_message": "➈ ā˛Ēāŗā˛°ā˛Žā˛žā˛Ŗā˛Ēā˛¤āŗā˛°ā˛•āŗā˛•ā˛žā˛—ā˛ŋ ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ", + "client_cert_password_title": "ā˛Ēāŗā˛°ā˛Žā˛žā˛Ŗā˛Ēā˛¤āŗā˛° ā˛Ēā˛žā˛¸āŗā˛ĩā˛°āŗā˛Ąāŗ", + "client_cert_remove_msg": "ā˛•āŗā˛˛āŗˆā˛‚ā˛Ÿāŗ ā˛Ēāŗā˛°ā˛Žā˛žā˛Ŗā˛Ēā˛¤āŗā˛°ā˛ĩā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", "client_cert_subtitle": "PKCS12 (.p12, .pfx) ā˛¸āŗā˛ĩ➰⺂ā˛Ēā˛ĩā˛¨āŗā˛¨āŗ ā˛Žā˛žā˛¤āŗā˛° ā˛Ŧ⺆➂ā˛Ŧ➞ā˛ŋā˛¸āŗā˛¤āŗā˛¤ā˛Ļāŗ†. ā˛˛ā˛žā˛—ā˛ŋā˛¨āŗ ➆➗⺁ā˛ĩ ā˛ŽāŗŠā˛Ļ➞⺁ ā˛Žā˛žā˛¤āŗā˛° ā˛Ēāŗā˛°ā˛Žā˛žā˛Ŗā˛Ēā˛¤āŗā˛° ā˛†ā˛Žā˛Ļ⺁/➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•āŗā˛ĩā˛ŋ➕⺆ ā˛˛ā˛­āŗā˛¯ā˛ĩā˛ŋā˛Ļāŗ†", + "client_cert_title": "SSL ā˛•āŗā˛˛āŗˆā˛‚ā˛Ÿāŗ ā˛Ēāŗā˛°ā˛Žā˛žā˛Ŗā˛Ēā˛¤āŗā˛° [ā˛Ēāŗā˛°ā˛žā˛¯āŗ‹ā˛—ā˛ŋ➕]", "clockwise": "ā˛•āŗā˛˛ā˛žā˛•āŗâ€Œā˛ĩāŗˆā˛¸āŗ", "close": "ā˛Žāŗā˛šāŗā˛šā˛ŋ", "collapse": "ā˛•āŗā˛—āŗā˛—ā˛ŋ➏⺁", + "collapse_all": "ā˛Žā˛˛āŗā˛˛ā˛ĩā˛¨āŗā˛¨āŗ ā˛•āŗā˛—āŗā˛—ā˛ŋ➏ā˛ŋ", "color": "ā˛Ŧā˛Ŗāŗā˛Ŗ", + "color_theme": "ā˛Ŧā˛Ŗāŗā˛Ŗ ā˛Ĩāŗ€ā˛Žāŗ", + "command": "ā˛†ā˛œāŗā˛žāŗ†", "command_palette_prompt": "ā˛Ēāŗā˛Ÿā˛—ā˛ŗāŗ, ā˛•āŗā˛°ā˛ŋ➝⺆➗➺⺁ ➅ā˛Ĩā˛ĩā˛ž ā˛†ā˛œāŗā˛žāŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¤āŗā˛ĩ➰ā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛šāŗā˛Ąāŗā˛•ā˛ŋ", + "command_palette_to_close": "ā˛Žāŗā˛šāŗā˛šā˛˛āŗ", + "command_palette_to_navigate": "ā˛Ēāŗā˛°ā˛ĩāŗ‡ā˛ļā˛ŋ➏➞⺁", "confirm": "ā˛Ļ⺃ā˛ĸ⺀➕➰ā˛ŋ➏ā˛ŋ", "confirm_delete_face": "➍⺀ā˛ĩ⺁ ā˛¸āŗā˛ĩā˛¤āŗā˛¤ā˛ŋ➍ā˛ŋ➂ā˛Ļ {name} ā˛Žāŗā˛–ā˛ĩā˛¨āŗā˛¨āŗ ➅➺ā˛ŋ➏➞⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", "confirm_delete_shared_link": "➈ ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛‚ā˛Ą ➞ā˛ŋā˛‚ā˛•āŗ ā˛…ā˛¨āŗā˛¨āŗ ➅➺ā˛ŋ➏➞⺁ ➍⺀ā˛ĩ⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", @@ -679,30 +760,81 @@ "contain": "ā˛’ā˛ŗā˛—āŗŠā˛‚ā˛Ąā˛ŋā˛°āŗā˛¤āŗā˛¤ā˛Ļāŗ†", "context": "➏➂ā˛Ļā˛°āŗā˛­", "continue": "ā˛Žāŗā˛‚ā˛Ļ⺁ā˛ĩ➰ā˛ŋ➏ā˛ŋ", + "control_bottom_app_bar_edit_time": "ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛• ā˛Žā˛¤āŗā˛¤āŗ ā˛¸ā˛Žā˛¯ā˛ĩā˛¨āŗā˛¨āŗ ➏➂ā˛Ēā˛žā˛Ļā˛ŋ➏ā˛ŋ", + "control_bottom_app_bar_share_to": "ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛˛āŗ", + "control_bottom_app_bar_trash_from_immich": "➅➍⺁ā˛Ēā˛¯āŗā˛•āŗā˛¤ā˛•āŗā˛•āŗ† ➏➰ā˛ŋ➏ā˛ŋ", "copied_image_to_clipboard": "➚ā˛ŋā˛¤āŗā˛°ā˛ĩā˛¨āŗā˛¨āŗ ā˛•āŗā˛˛ā˛ŋā˛Ēāŗâ€Œā˛Ŧāŗ‹ā˛°āŗā˛Ąāŗâ€Œā˛—āŗ† ➍➕➞ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†.", + "copied_to_clipboard": "ā˛•āŗā˛˛ā˛ŋā˛Ēāŗ ā˛Ŧāŗ‹ā˛°āŗā˛Ąāŗ ➗⺆ ➍➕➞ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†!", + "copy_error": "ā˛Ļ⺋➎ā˛ĩā˛¨āŗā˛¨āŗ ➍➕➞ā˛ŋ➏ā˛ŋ", + "copy_file_path": "ā˛Ģāŗˆā˛˛āŗ ā˛Žā˛žā˛°āŗā˛—ā˛ĩā˛¨āŗā˛¨āŗ ➍➕➞ā˛ŋ➏ā˛ŋ", + "copy_image": "➚ā˛ŋā˛¤āŗā˛°ā˛ĩā˛¨āŗā˛¨āŗ ➍➕➞ā˛ŋ➏ā˛ŋ", + "copy_link": "➞ā˛ŋā˛‚ā˛•āŗ ➍➕➞ā˛ŋ➏ā˛ŋ", "copy_link_to_clipboard": "➞ā˛ŋā˛‚ā˛•āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛•āŗā˛˛ā˛ŋā˛Ēāŗâ€Œā˛Ŧāŗ‹ā˛°āŗā˛Ąāŗâ€Œā˛—āŗ† ➍➕➞ā˛ŋ➏ā˛ŋ", + "copy_password": "ā˛Ēā˛žā˛¸āŗā˛ĩā˛°āŗā˛Ąāŗ ➍➕➞ā˛ŋ➏ā˛ŋ", + "copy_to_clipboard": "ā˛•āŗā˛˛ā˛ŋā˛Ēāŗ ā˛Ŧāŗ‹ā˛°āŗā˛Ąāŗ ➗⺆ ➍➕➞ā˛ŋ➏ā˛ŋ", "country": "ā˛Ļāŗ‡ā˛ļ", "cover": "➕ā˛ĩā˛°āŗ", "covers": "➕ā˛ĩā˛°āŗâ€Œā˛—ā˛ŗāŗ", "create": "➰➚ā˛ŋ➏ā˛ŋ", + "create_album": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ➰➚ā˛ŋ➏ā˛ŋ", + "create_album_page_untitled": "ā˛ļāŗ€ā˛°āŗā˛ˇā˛ŋā˛•āŗ†ā˛°ā˛šā˛ŋ➤", + "create_api_key": "➰➚ā˛ŋ➏ā˛ŋ API ➕⺀", + "create_first_workflow": "ā˛ŽāŗŠā˛Ļ➞ ➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩā˛¨āŗā˛¨āŗ ➰➚ā˛ŋ➏ā˛ŋ", + "create_library": "ā˛—āŗā˛°ā˛‚ā˛Ĩā˛žā˛˛ā˛¯ā˛ĩā˛¨āŗā˛¨āŗ ➰➚ā˛ŋ➏ā˛ŋ", + "create_link": "➞ā˛ŋā˛‚ā˛•āŗ ➰➚ā˛ŋ➏ā˛ŋ", "create_link_to_share": "ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛˛āŗ ➞ā˛ŋā˛‚ā˛•āŗ ➰➚ā˛ŋ➏ā˛ŋ", + "create_link_to_share_description": "➞ā˛ŋā˛‚ā˛•āŗ ā˛šāŗŠā˛‚ā˛Ļā˛ŋ➰⺁ā˛ĩ ā˛¯ā˛žā˛°ā˛žā˛Ļ➰⺂ ā˛†ā˛¯āŗā˛Ļ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¨āŗ‹ā˛Ąā˛˛ā˛ŋ", + "create_new": "ā˛šāŗŠā˛¸ā˛Ļā˛¨āŗā˛¨āŗ ➰➚ā˛ŋ➏ā˛ŋ", + "create_new_person": "ā˛šāŗŠā˛¸ ā˛ĩāŗā˛¯ā˛•āŗā˛¤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➰➚ā˛ŋ➏ā˛ŋ", "create_new_person_hint": "ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋā˛Ļ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛šāŗŠā˛¸ ā˛ĩāŗā˛¯ā˛•āŗā˛¤ā˛ŋ➗⺆ ➍ā˛ŋā˛¯āŗ‹ā˛œā˛ŋ➏ā˛ŋ", + "create_new_user": "ā˛šāŗŠā˛¸ ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛¨āŗā˛¨āŗ ➰➚ā˛ŋ➏ā˛ŋ", + "create_shared_album_page_share_add_assets": "ā˛Žā˛Ąā˛ŋā˛Ąā˛ŋ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗāŗ", + "create_shared_album_page_share_select_photos": "ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋ", + "create_shared_link": "ā˛šā˛‚ā˛šā˛ŋā˛Ļ ➞ā˛ŋā˛‚ā˛•āŗ ➰➚ā˛ŋ➏ā˛ŋ", + "create_tag": "ā˛Ÿāŗā˛¯ā˛žā˛—āŗ ➰➚ā˛ŋ➏ā˛ŋ", "create_tag_description": "ā˛šāŗŠā˛¸ ā˛Ÿāŗā˛¯ā˛žā˛—āŗ ➰➚ā˛ŋ➏ā˛ŋ. ā˛¨āŗ†ā˛¸āŗā˛Ÿāŗ†ā˛Ąāŗ ā˛Ÿāŗā˛¯ā˛žā˛—āŗâ€Œā˛—ā˛ŗā˛ŋā˛—ā˛žā˛—ā˛ŋ, ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ā˛Ģā˛žā˛°āŗā˛ĩā˛°āŗā˛Ąāŗ ā˛¸āŗā˛˛āŗā˛¯ā˛žā˛ļāŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛’ā˛ŗā˛—āŗŠā˛‚ā˛Ąā˛‚ā˛¤āŗ† ā˛Ÿāŗā˛¯ā˛žā˛—āŗâ€Œā˛¨ ā˛Ēāŗ‚ā˛°āŗā˛Ŗ ā˛Žā˛žā˛°āŗā˛—ā˛ĩā˛¨āŗā˛¨āŗ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ.", + "create_user": "ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛¨āŗā˛¨āŗ ➰➚ā˛ŋ➏ā˛ŋ", + "create_workflow": "➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩā˛¨āŗā˛¨āŗ ➰➚ā˛ŋ➏ā˛ŋ", "created": "➰➚ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "created_at": "➰➚ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "creating_linked_albums": "➞ā˛ŋā˛‚ā˛•āŗā˛Ąāŗ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➰➚ā˛ŋ➏⺁ā˛ĩ⺁ā˛Ļ⺁ ...", + "crop": "ā˛Ŧ⺆➺⺆", + "crop_aspect_ratio_fixed": "ā˛¸āŗā˛Ĩā˛ŋ➰", + "crop_aspect_ratio_free": "ā˛‰ā˛šā˛ŋ➤", + "crop_aspect_ratio_original": "ā˛Žāŗ‚ā˛˛", + "crop_aspect_ratio_square": "ā˛šāŗŒā˛•", + "curated_object_page_title": "ā˛ĩā˛ŋ➎➝➗➺⺁", + "current_device": "ā˛Ēāŗā˛°ā˛¸āŗā˛¤āŗā˛¤ ā˛¸ā˛žā˛§ā˛¨", + "current_pin_code": "ā˛Ēāŗā˛°ā˛¸āŗā˛¤āŗā˛¤ ā˛Ēā˛ŋā˛¨āŗ ā˛•āŗ‹ā˛Ąāŗ", + "current_server_address": "ā˛Ēāŗā˛°ā˛¸āŗā˛¤āŗā˛¤ ā˛¸ā˛°āŗā˛ĩā˛°āŗ ā˛ĩā˛ŋā˛ŗā˛žā˛¸", + "custom_date": "ā˛•ā˛¸āŗā˛Ÿā˛Žāŗ ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛•", + "custom_locale": "ā˛•ā˛¸āŗā˛Ÿā˛Žāŗ ā˛˛āŗŠā˛•āŗ‡ā˛˛āŗ", + "custom_locale_description": "ā˛†ā˛¯āŗā˛Ļ ā˛­ā˛žā˛ˇāŗ† ā˛Žā˛¤āŗā˛¤āŗ ā˛Ēāŗā˛°ā˛Ļāŗ‡ā˛ļā˛ĩā˛¨āŗā˛¨āŗ ➆➧➰ā˛ŋ➏ā˛ŋā˛Ļ ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛•ā˛—ā˛ŗāŗ, ā˛¸ā˛Žā˛¯ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛¸ā˛‚ā˛–āŗā˛¯āŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ģā˛žā˛°āŗā˛Žāŗā˛¯ā˛žā˛Ÿāŗ ā˛Žā˛žā˛Ąā˛ŋ", + "custom_url": "ā˛•ā˛¸āŗā˛Ÿā˛Žāŗ URL", "cutoff_date_description": "ā˛šā˛ŋ➂ā˛Ļā˛ŋ➍ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➇➰ā˛ŋ➏ā˛ŋâ€Ļ", "dark": "ā˛•ā˛¤āŗā˛¤ā˛˛āŗ", + "dark_theme": "ā˛Ąā˛žā˛°āŗā˛•āŗ ā˛Ĩāŗ€ā˛Žāŗ ➗⺆ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏ā˛ŋ", + "date": "ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛•", + "date_and_time": "ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛• ā˛Žā˛¤āŗā˛¤āŗ ā˛¸ā˛Žā˛¯", + "date_before": "ā˛ŽāŗŠā˛Ļ➞⺁ ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛•", "date_of_birth_saved": "ā˛œā˛¨āŗā˛Ž ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛•ā˛ĩā˛¨āŗā˛¨āŗ ➝ā˛ļā˛¸āŗā˛ĩā˛ŋā˛¯ā˛žā˛—ā˛ŋ ➉➺ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "date_range": "ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛• ā˛ļāŗā˛°āŗ‡ā˛Ŗā˛ŋ", "day": "ā˛Ļā˛ŋ➍", - "deduplication_criteria_1": "➚ā˛ŋā˛¤āŗā˛°ā˛Ļ ā˛—ā˛žā˛¤āŗā˛° ā˛Ŧāŗˆā˛Ÿāŗâ€Œā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ", - "deduplication_criteria_2": "EXIF ā˛Ąāŗ‡ā˛Ÿā˛žā˛Ļ ā˛Žā˛Ŗā˛ŋ➕⺆", - "deduplication_info_description": "ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¸āŗā˛ĩā˛¯ā˛‚ā˛šā˛žā˛˛ā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ēāŗ‚ā˛°āŗā˛ĩ ā˛†ā˛¯āŗā˛•āŗ† ā˛Žā˛žā˛Ąā˛˛āŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛¨ā˛•ā˛˛āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛ĻāŗŠā˛Ąāŗā˛Ą ā˛Ēāŗā˛°ā˛Žā˛žā˛Ŗā˛Ļā˛˛āŗā˛˛ā˛ŋ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛āŗ, ā˛¨ā˛žā˛ĩ⺁ ā˛‡ā˛˛āŗā˛˛ā˛ŋ ā˛¨āŗ‹ā˛Ąāŗā˛¤āŗā˛¤āŗ‡ā˛ĩāŗ†:", + "days": "ā˛Ļā˛ŋ➍➗➺⺁", + "deduplicate_all": "ā˛Žā˛˛āŗā˛˛ā˛ĩā˛¨āŗā˛¨āŗ‚ ā˛¸ā˛Žā˛°āŗā˛Ēā˛ŋ➏ā˛ŋ", + "default_locale": "ā˛Ąāŗ€ā˛Ģā˛žā˛˛āŗā˛Ÿāŗ ā˛˛āŗŠā˛•āŗ‡ā˛˛āŗ", + "default_locale_description": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ŧāŗā˛°āŗŒā˛¸ā˛°āŗ ā˛˛āŗŠā˛•āŗ‡ā˛˛āŗ ā˛†ā˛§ā˛žā˛°ā˛ŋ➤ ā˛¸āŗā˛ĩ➰⺂ā˛Ē ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛•ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛¸ā˛‚ā˛–āŗā˛¯āŗ†ā˛—ā˛ŗāŗ", "delete": "➅➺ā˛ŋ➏ā˛ŋ", "delete_action_confirmation_message": "➍⺀ā˛ĩ⺁ ➈ ā˛¸āŗā˛ĩā˛¤āŗā˛¤ā˛¨āŗā˛¨āŗ ➅➺ā˛ŋ➏➞⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž? ➈ ā˛•āŗā˛°ā˛ŋ➝⺆➝⺁ ā˛¸āŗā˛ĩā˛¤āŗā˛¤ā˛¨āŗā˛¨āŗ ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛¨ ➅➍⺁ā˛Ēā˛¯āŗā˛•āŗā˛¤ā˛•āŗā˛•āŗ† ➏➰ā˛ŋā˛¸āŗā˛¤āŗā˛¤ā˛Ļāŗ† ā˛Žā˛¤āŗā˛¤āŗ ➍⺀ā˛ĩ⺁ ➅ā˛Ļā˛¨āŗā˛¨āŗ ā˛¸āŗā˛Ĩ➺⺀➝ā˛ĩā˛žā˛—ā˛ŋ ➅➺ā˛ŋ➏➞⺁ ā˛Ŧ➝➏ā˛ŋā˛Ļ➰⺆ ā˛•āŗ‡ā˛ŗāŗā˛¤āŗā˛¤ā˛Ļāŗ†", + "delete_album": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ➅➺ā˛ŋ➏ā˛ŋ", "delete_api_key_prompt": "➈ API ➕⺀➞ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➅➺ā˛ŋ➏➞⺁ ➍⺀ā˛ĩ⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", "delete_dialog_alert": "➈ ā˛ā˛Ÿā˛‚ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ā˛Žā˛¤āŗā˛¤āŗ ➍ā˛ŋā˛Žāŗā˛Ž ā˛¸ā˛žā˛§ā˛¨ā˛Ļā˛ŋ➂ā˛Ļ ā˛ļā˛žā˛ļāŗā˛ĩ➤ā˛ĩā˛žā˛—ā˛ŋ ➅➺ā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†", "delete_dialog_alert_local": "➈ ā˛ā˛Ÿā˛‚ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛Žāŗā˛Ž ā˛¸ā˛žā˛§ā˛¨ā˛Ļā˛ŋ➂ā˛Ļ ā˛ļā˛žā˛ļāŗā˛ĩ➤ā˛ĩā˛žā˛—ā˛ŋ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ† ➆ā˛Ļ➰⺆ ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ ā˛‡ā˛¨āŗā˛¨āŗ‚ ā˛˛ā˛­āŗā˛¯ā˛ĩā˛ŋā˛°āŗā˛¤āŗā˛¤ā˛Ļāŗ†", "delete_dialog_alert_local_non_backed_up": "➕⺆➞ā˛ĩ⺁ ā˛ā˛Ÿā˛‚ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗâ€Œā˛—āŗ† ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—ā˛ŋā˛˛āŗā˛˛ ā˛Žā˛¤āŗā˛¤āŗ ➍ā˛ŋā˛Žāŗā˛Ž ā˛¸ā˛žā˛§ā˛¨ā˛Ļā˛ŋ➂ā˛Ļ ā˛ļā˛žā˛ļāŗā˛ĩ➤ā˛ĩā˛žā˛—ā˛ŋ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†", + "delete_dialog_alert_remote": "➈ ā˛ā˛Ÿā˛‚ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ ā˛ļā˛žā˛ļāŗā˛ĩ➤ā˛ĩā˛žā˛—ā˛ŋ ➅➺ā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†", "delete_duplicates_confirmation": "➍⺀ā˛ĩ⺁ ➈ ā˛¨ā˛•ā˛˛āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛ļā˛žā˛ļāŗā˛ĩ➤ā˛ĩā˛žā˛—ā˛ŋ ➅➺ā˛ŋ➏➞⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", + "delete_local_dialog_ok_backed_up_only": "ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛ŋ➰⺁ā˛ĩ⺁ā˛Ļā˛¨āŗā˛¨āŗ ā˛Žā˛žā˛¤āŗā˛° ➅➺ā˛ŋ➏ā˛ŋ", + "delete_tag_confirmation_prompt": "➍⺀ā˛ĩ⺁ {tagName} ā˛Ÿāŗā˛¯ā˛žā˛—āŗ ā˛…ā˛¨āŗā˛¨āŗ ➅➺ā˛ŋ➏➞⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", "deletes_missing_assets": "ā˛Ąā˛ŋā˛¸āŗā˛•āŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ ā˛•ā˛žā˛Ŗāŗ†ā˛¯ā˛žā˛Ļ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➅➺ā˛ŋā˛¸āŗā˛¤āŗā˛¤ā˛Ļāŗ†", "description": "ā˛ĩā˛ŋā˛ĩ➰➪⺆", "description_input_submit_error": "ā˛ĩā˛ŋā˛ĩā˛°ā˛Ŗāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➍ā˛ĩ⺀➕➰ā˛ŋ➏⺁ā˛ĩā˛˛āŗā˛˛ā˛ŋ ā˛Ļ⺋➎, ā˛šāŗ†ā˛šāŗā˛šā˛ŋ➍ ā˛ĩā˛ŋā˛ĩ➰➗➺ā˛ŋā˛—ā˛žā˛—ā˛ŋ ā˛˛ā˛žā˛—āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ē➰ā˛ŋā˛ļ⺀➞ā˛ŋ➏ā˛ŋ", @@ -723,6 +855,7 @@ "downloading": "ā˛ĄāŗŒā˛¨āŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", "drop_files_to_upload": "➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛Ģāŗˆā˛˛āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Žā˛˛āŗā˛˛ā˛ŋā˛¯ā˛žā˛Ļ➰⺂ ā˛Ŧā˛ŋā˛Ąā˛ŋ", "duplicates": "➍➕➞⺁➗➺⺁", + "duplicates_description": "ā˛Ēāŗā˛°ā˛¤ā˛ŋā˛¯āŗŠā˛‚ā˛Ļ⺁ ➗⺁➂ā˛Ēā˛¨āŗā˛¨āŗ, ā˛¯ā˛žā˛ĩ⺁ā˛Ļā˛žā˛Ļ➰⺂ ➇ā˛Ļāŗā˛Ļ➰⺆, ➍➕➞⺁➗➺⺁ ā˛Žā˛‚ā˛Ļ⺁ ā˛¸āŗ‚ā˛šā˛ŋ➏⺁ā˛ĩ ā˛Žāŗ‚ā˛˛ā˛• ā˛Ē➰ā˛ŋā˛šā˛°ā˛ŋ➏ā˛ŋ.", "duration": "➅ā˛ĩ➧ā˛ŋ", "edit": "➤ā˛ŋā˛Ļāŗā˛Ļ⺁", "edit_date_and_time": "ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛• ā˛Žā˛¤āŗā˛¤āŗ ā˛¸ā˛Žā˛¯ā˛ĩā˛¨āŗā˛¨āŗ ➏➂ā˛Ēā˛žā˛Ļā˛ŋ➏ā˛ŋ", @@ -733,6 +866,7 @@ "editor_close_without_save_prompt": "ā˛Ŧā˛Ļā˛˛ā˛žā˛ĩā˛Ŗāŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➉➺ā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛ĩ⺁ā˛Ļā˛ŋā˛˛āŗā˛˛", "editor_confirm_reset_all_changes": "➍⺀ā˛ĩ⺁ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛Ŧā˛Ļā˛˛ā˛žā˛ĩā˛Ŗāŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗā˛šāŗŠā˛‚ā˛Ļā˛ŋ➏➞⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", "email": "ā˛‡ā˛Žāŗ†āŗ•ā˛˛āŗ", + "empty_folder": "➈ ā˛Ģāŗ‹ā˛˛āŗā˛Ąā˛°āŗ ā˛–ā˛žā˛˛ā˛ŋā˛¯ā˛žā˛—ā˛ŋā˛Ļāŗ†", "empty_trash_confirmation": "➍⺀ā˛ĩ⺁ ➕➏ā˛ĩā˛¨āŗā˛¨āŗ ā˛–ā˛žā˛˛ā˛ŋ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž? ➇ā˛Ļ⺁ ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ ➕➏ā˛Ļā˛˛āŗā˛˛ā˛ŋ➰⺁ā˛ĩ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛ļā˛žā˛ļāŗā˛ĩ➤ā˛ĩā˛žā˛—ā˛ŋ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•āŗā˛¤āŗā˛¤ā˛Ļāŗ†.\n➍⺀ā˛ĩ⺁ ➈ ā˛•āŗā˛°ā˛ŋā˛¯āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➰ā˛Ļāŗā˛Ļāŗā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛!", "enable": "ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", "enable_biometric_auth_description": "ā˛Ŧā˛¯āŗ‹ā˛Žāŗ†ā˛Ÿāŗā˛°ā˛ŋā˛•āŗ ā˛Ļ⺃ā˛ĸ⺀➕➰➪ā˛ĩā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ➍ā˛ŋā˛Žāŗā˛Ž ā˛Ēā˛ŋā˛¨āŗ ā˛•āŗ‹ā˛Ąāŗ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ", @@ -753,12 +887,14 @@ "error_adding_users_to_album": "ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛¨āŗā˛¨āŗ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—āŗ† ➏⺇➰ā˛ŋ➏⺁ā˛ĩā˛˛āŗā˛˛ā˛ŋ ā˛Ļ⺋➎", "error_deleting_shared_user": "ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛‚ā˛Ą ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛¨āŗā˛¨āŗ ➅➺ā˛ŋ➏⺁ā˛ĩā˛˛āŗā˛˛ā˛ŋ ā˛Ļ⺋➎", "error_hiding_buy_button": "➖➰⺀ā˛Ļā˛ŋ ā˛Ŧā˛Ÿā˛¨āŗ ā˛Žā˛°āŗ†ā˛Žā˛žā˛Ąāŗā˛ĩā˛˛āŗā˛˛ā˛ŋ ā˛Ļ⺋➎", + "error_removing_assets_from_album": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•āŗā˛ĩā˛˛āŗā˛˛ā˛ŋ ā˛Ļ⺋➎, ā˛šāŗ†ā˛šāŗā˛šā˛ŋ➍ ā˛ĩā˛ŋā˛ĩ➰➗➺ā˛ŋā˛—ā˛žā˛—ā˛ŋ ā˛•ā˛¨āŗā˛¸āŗ‹ā˛˛āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ē➰ā˛ŋā˛ļ⺀➞ā˛ŋ➏ā˛ŋ", "error_selecting_all_assets": "ā˛Žā˛˛āŗā˛˛ā˛ž ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛†ā˛¯āŗā˛•āŗ† ā˛Žā˛žā˛Ąāŗā˛ĩā˛žā˛— ā˛Ļ⺋➎ ā˛‰ā˛‚ā˛Ÿā˛žā˛—ā˛ŋā˛Ļāŗ†", "exclusion_pattern_already_exists": "➈ ā˛šāŗŠā˛°ā˛—ā˛ŋā˛Ąāŗā˛ĩ ā˛Žā˛žā˛Ļ➰ā˛ŋ ā˛ˆā˛—ā˛žā˛—ā˛˛āŗ‡ ā˛…ā˛¸āŗā˛¤ā˛ŋā˛¤āŗā˛ĩā˛Ļā˛˛āŗā˛˛ā˛ŋā˛Ļāŗ†.", "failed_to_create_album": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ➰➚ā˛ŋ➏➞⺁ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", "failed_to_create_shared_link": "ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛‚ā˛Ą ➞ā˛ŋā˛‚ā˛•āŗ ➰➚ā˛ŋ➏➞⺁ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", "failed_to_edit_shared_link": "ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛‚ā˛Ą ➞ā˛ŋā˛‚ā˛•āŗ ā˛…ā˛¨āŗā˛¨āŗ ➏➂ā˛Ēā˛žā˛Ļā˛ŋ➏➞⺁ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", "failed_to_get_people": "ā˛œā˛¨ā˛°ā˛¨āŗā˛¨āŗ ā˛Ēā˛Ąāŗ†ā˛¯āŗā˛ĩā˛˛āŗā˛˛ā˛ŋ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", + "failed_to_keep_this_delete_others": "➈ ā˛¸āŗā˛ĩā˛¤āŗā˛¤ā˛¨āŗā˛¨āŗ ➉➺ā˛ŋ➏ā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛˛āŗ ā˛Žā˛¤āŗā˛¤āŗ ➇➤➰ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➅➺ā˛ŋ➏➞⺁ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", "failed_to_load_asset": "ā˛¸āŗā˛ĩā˛¤āŗā˛¤ā˛¨āŗā˛¨āŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", "failed_to_load_assets": "ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", "failed_to_load_people": "ā˛œā˛¨ā˛°ā˛¨āŗā˛¨āŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", @@ -770,6 +906,7 @@ "incorrect_email_or_password": "➤ā˛Ēāŗā˛Ēā˛žā˛Ļ ā˛‡ā˛Žāŗ‡ā˛˛āŗ ➅ā˛Ĩā˛ĩā˛ž ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ", "library_folder_already_exists": "➈ ā˛†ā˛Žā˛Ļ⺁ ā˛Žā˛žā˛°āŗā˛—ā˛ĩ⺁ ā˛ˆā˛—ā˛žā˛—ā˛˛āŗ‡ ā˛…ā˛¸āŗā˛¤ā˛ŋā˛¤āŗā˛ĩā˛Ļā˛˛āŗā˛˛ā˛ŋā˛Ļāŗ†.", "profile_picture_transparent_pixels": "ā˛Ēāŗā˛°āŗŠā˛Ģāŗˆā˛˛āŗ ➚ā˛ŋā˛¤āŗā˛°ā˛—ā˛ŗāŗ ā˛Ēā˛žā˛°ā˛Ļā˛°āŗā˛ļ➕ ā˛Ēā˛ŋā˛•āŗā˛¸āŗ†ā˛˛āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛šāŗŠā˛‚ā˛Ļā˛ŋ➰ā˛Ŧā˛žā˛°ā˛Ļ⺁. ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ➚ā˛ŋā˛¤āŗā˛°ā˛ĩā˛¨āŗā˛¨āŗ ā˛œāŗ‚ā˛Žāŗ ā˛‡ā˛¨āŗ ā˛Žā˛žā˛Ąā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ/➅ā˛Ĩā˛ĩā˛ž ➏➰ā˛ŋ➏ā˛ŋ.", + "quota_higher_than_disk_size": "➍⺀ā˛ĩ⺁ ā˛Ąā˛ŋā˛¸āŗā˛•āŗ ā˛—ā˛žā˛¤āŗā˛°ā˛•āŗā˛•ā˛ŋ➂➤ ā˛šāŗ†ā˛šāŗā˛šā˛ŋ➍ ā˛•āŗ‹ā˛Ÿā˛žā˛ĩā˛¨āŗā˛¨āŗ ā˛šāŗŠā˛‚ā˛Ļā˛ŋ➏ā˛ŋā˛Ļāŗā˛Ļ⺀➰ā˛ŋ", "unable_to_add_album_users": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—āŗ† ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛˛āŗā˛˛", "unable_to_add_assets_to_shared_link": "ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛‚ā˛Ą ➞ā˛ŋā˛‚ā˛•āŗâ€Œā˛—āŗ† ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛˛āŗā˛˛", "unable_to_add_comment": "ā˛•ā˛žā˛Žāŗ†ā˛‚ā˛Ÿāŗ ➏⺇➰ā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛˛āŗā˛˛", @@ -843,15 +980,23 @@ "unable_to_upload_file": "ā˛Ģāŗˆā˛˛āŗ ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛˛āŗā˛˛" }, "exif": "ā˛Žā˛•āŗā˛¸ā˛ŋā˛Ģāŗ", + "experimental_settings_new_asset_list_title": "ā˛Ēāŗā˛°ā˛žā˛¯āŗ‹ā˛—ā˛ŋ➕ ā˛Ģāŗ‹ā˛Ÿāŗ‹ ā˛—āŗā˛°ā˛ŋā˛Ąāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", "experimental_settings_subtitle": "➍ā˛ŋā˛Žāŗā˛Ž ā˛¸āŗā˛ĩ➂➤ ➅ā˛Ēā˛žā˛¯ā˛Ļā˛˛āŗā˛˛ā˛ŋ ā˛Ŧ➺➏ā˛ŋ!", "expired": "➅ā˛ĩ➧ā˛ŋ ā˛Žāŗ€ā˛°ā˛ŋā˛Ļāŗ†", "explore": "ā˛Ē➰ā˛ŋā˛ļ⺋➧ā˛ŋ➏⺁", "explorer": "ā˛Žā˛•āŗā˛¸āŗâ€Œā˛Ēāŗā˛˛āŗ‹ā˛°ā˛°āŗ", + "export": "➰ā˛Ģāŗā˛¤āŗ", + "extension": "ā˛ĩā˛ŋā˛¸āŗā˛¤ā˛°ā˛Ŗāŗ†", + "external": "ā˛Ŧā˛žā˛šāŗā˛¯", "external_network_sheet_info": "➍⺀ā˛ĩ⺁ ➆ā˛Ļāŗā˛¯ā˛¤āŗ†ā˛¯ ā˛ĩ⺈-ā˛Ģ⺈ ā˛¨āŗ†ā˛Ÿāŗâ€Œā˛ĩā˛°āŗā˛•āŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ ā˛‡ā˛˛āŗā˛˛ā˛Ļā˛ŋ➰⺁ā˛ĩā˛žā˛—, ➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ā˛Žāŗ‡ā˛˛ā˛ŋ➍ā˛ŋ➂ā˛Ļ ā˛•āŗ†ā˛ŗā˛•āŗā˛•āŗ† ➤➞⺁ā˛Ēā˛Ŧā˛šāŗā˛Ļā˛žā˛Ļ ➕⺆➺➗ā˛ŋ➍ URL ā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ ā˛ŽāŗŠā˛Ļ➞➍⺆➝ā˛Ļ➰ ā˛Žāŗ‚ā˛˛ā˛• ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛—āŗ† ➏➂ā˛Ēā˛°āŗā˛•ā˛—āŗŠā˛ŗāŗā˛ŗāŗā˛¤āŗā˛¤ā˛Ļāŗ†", "face_unassigned": "➍ā˛ŋā˛¯āŗ‹ā˛œā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛˛āŗā˛˛", "failed_to_load_assets": "ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", + "failed_to_load_folder": "ā˛Ģāŗ‹ā˛˛āŗā˛Ąā˛°āŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", + "favorite": "ā˛¨āŗ†ā˛šāŗā˛šā˛ŋ➍", "favorite_or_unfavorite_photo": "ā˛¨āŗ†ā˛šāŗā˛šā˛ŋ➍ ➅ā˛Ĩā˛ĩā˛ž ā˛Žāŗ†ā˛šāŗā˛šā˛ŋ➍ā˛Ļ➰ā˛ŋ➂ā˛Ļ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛ŋā˛Ļ ā˛Ģāŗ‹ā˛Ÿāŗ‹", "favorites": "ā˛Žāŗ†ā˛šāŗā˛šā˛ŋ➍ā˛ĩ⺁➗➺⺁", + "favorites_page_no_favorites": "ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛¨āŗ†ā˛šāŗā˛šā˛ŋ➍ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗāŗ ā˛•ā˛‚ā˛Ąāŗā˛Ŧ➂ā˛Ļā˛ŋā˛˛āŗā˛˛", + "features": "ā˛ĩ⺈ā˛ļā˛ŋā˛ˇāŗā˛Ÿāŗā˛¯ā˛—ā˛ŗāŗ", "features_setting_description": "➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ā˛ĩ⺈ā˛ļā˛ŋā˛ˇāŗā˛Ÿāŗā˛¯ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", "file_name_or_extension": "ā˛Ģāŗˆā˛˛āŗ ā˛šāŗ†ā˛¸ā˛°āŗ ➅ā˛Ĩā˛ĩā˛ž ā˛ĩā˛ŋā˛¸āŗā˛¤ā˛°ā˛Ŗāŗ†", "filename": "ā˛Ģāŗˆā˛˛āŗ ā˛šāŗ†ā˛¸ā˛°āŗ", @@ -866,9 +1011,11 @@ "general": "ā˛œā˛¨ā˛°ā˛˛āŗ", "geolocation_instruction_location": "GPS ➍ā˛ŋā˛°āŗā˛Ļāŗ‡ā˛ļā˛žā˛‚ā˛•ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛šāŗŠā˛‚ā˛Ļā˛ŋ➰⺁ā˛ĩ ā˛¸āŗā˛ĩā˛¤āŗā˛¤ā˛ŋ➍ ā˛¸āŗā˛Ĩ➺ā˛ĩā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏➞⺁ ➅ā˛Ļ➰ ā˛Žāŗ‡ā˛˛āŗ† ā˛•āŗā˛˛ā˛ŋā˛•āŗ ā˛Žā˛žā˛Ąā˛ŋ, ➅ā˛Ĩā˛ĩā˛ž ā˛¨ā˛•āŗā˛ˇāŗ†ā˛¯ā˛ŋ➂ā˛Ļ ➍⺇➰ā˛ĩā˛žā˛—ā˛ŋ ā˛¸āŗā˛Ĩ➺ā˛ĩā˛¨āŗā˛¨āŗ ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋ", "get_wifiname_error": "ā˛ĩ⺈-ā˛Ģ⺈ ā˛šāŗ†ā˛¸ā˛°ā˛¨āŗā˛¨āŗ ā˛Ēā˛Ąāŗ†ā˛¯ā˛˛āŗ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛žā˛—ā˛˛ā˛ŋā˛˛āŗā˛˛. ➍⺀ā˛ĩ⺁ ā˛…ā˛—ā˛¤āŗā˛¯ ā˛…ā˛¨āŗā˛Žā˛¤ā˛ŋā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¨āŗ€ā˛Ąā˛ŋā˛Ļāŗā˛Ļ⺀➰ā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩ⺈-ā˛Ģ⺈ ā˛¨āŗ†ā˛Ÿāŗâ€Œā˛ĩā˛°āŗā˛•āŗâ€Œā˛—āŗ† ➏➂ā˛Ēā˛°āŗā˛•ā˛—āŗŠā˛‚ā˛Ąā˛ŋā˛Ļāŗā˛Ļ⺀➰ā˛ŋ ā˛Žā˛‚ā˛Ļ⺁ ā˛–ā˛šā˛ŋ➤ā˛Ēā˛Ąā˛ŋ➏ā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛ŋ", + "header_settings_field_validator_msg": "ā˛ŽāŗŒā˛˛āŗā˛¯ ā˛–ā˛žā˛˛ā˛ŋā˛¯ā˛žā˛—ā˛ŋ➰ā˛Ŧā˛žā˛°ā˛Ļ⺁", "home_page_add_to_album_conflicts": "{album} ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—āŗ† {added} ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†. {failed} ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗāŗ ā˛ˆā˛—ā˛žā˛—ā˛˛āŗ‡ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋā˛ĩāŗ†.", "home_page_add_to_album_err_local": "ā˛¸āŗā˛Ĩ➺⺀➝ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—ā˛ŗā˛ŋ➗⺆ ➏⺇➰ā˛ŋ➏➞⺁ ā˛‡ā˛¨āŗā˛¨āŗ‚ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛˛āŗā˛˛, ā˛Ŧā˛ŋā˛Ÿāŗā˛Ÿāŗā˛Ŧā˛ŋā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", "home_page_add_to_album_success": "{album} ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—āŗ† {added} ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†.", + "home_page_album_err_partner": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—āŗ† ā˛Ēā˛žā˛˛āŗā˛Ļā˛žā˛° ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏➞⺁ ā˛‡ā˛¨āŗā˛¨āŗ‚ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛, ā˛Ŧā˛ŋā˛Ÿāŗā˛Ÿāŗā˛Ŧā˛ŋā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", "home_page_archive_err_local": "ā˛¸āŗā˛Ĩ➺⺀➝ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛‡ā˛¨āŗā˛¨āŗ‚ ā˛†ā˛°āŗā˛•āŗˆā˛ĩāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛, ā˛Ŧā˛ŋā˛Ÿāŗā˛Ÿāŗā˛Ŧā˛ŋā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", "home_page_archive_err_partner": "ā˛Ēā˛žā˛˛āŗā˛Ļā˛žā˛° ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛†ā˛°āŗā˛•āŗˆā˛ĩāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛, ā˛Ŧā˛ŋā˛Ÿāŗā˛Ÿāŗā˛Ŧā˛ŋā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", "home_page_delete_err_partner": "ā˛Ēā˛žā˛˛āŗā˛Ļā˛žā˛° ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➅➺ā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛, ā˛Ŧā˛ŋā˛Ÿāŗā˛Ÿāŗā˛Ŧā˛ŋā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", @@ -901,24 +1048,40 @@ "language": "ā˛­ā˛žā˛ˇāŗ†", "language_no_results_subtitle": "➍ā˛ŋā˛Žāŗā˛Ž ā˛šāŗā˛Ąāŗā˛•ā˛žā˛Ÿ ā˛Ēā˛Ļā˛ĩā˛¨āŗā˛¨āŗ ➏➰ā˛ŋā˛šāŗŠā˛‚ā˛Ļā˛ŋ➏➞⺁ ā˛Ēāŗā˛°ā˛¯ā˛¤āŗā˛¨ā˛ŋ➏ā˛ŋ", "language_setting_description": "➍ā˛ŋā˛Žāŗā˛Ž ➆ā˛Ļāŗā˛¯ā˛¤āŗ†ā˛¯ ā˛­ā˛žā˛ˇāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋ", + "latitude": "ā˛…ā˛•āŗā˛ˇā˛žā˛‚ā˛ļ", "leave": "ā˛Ŧā˛ŋā˛Ąā˛ŋ", "level": "ā˛Žā˛Ÿāŗā˛Ÿ", + "library": "ā˛—āŗā˛°ā˛‚ā˛Ĩā˛žā˛˛ā˛¯", "light": "ā˛Ŧ⺆➺➕⺁", "list": "ā˛Ēā˛Ÿāŗā˛Ÿā˛ŋ", + "loading": "ā˛˛āŗ‹ā˛Ąāŗ ā˛†ā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", "loading_search_results_failed": "ā˛šāŗā˛Ąāŗā˛•ā˛žā˛Ÿ ā˛Ģ➞ā˛ŋā˛¤ā˛žā˛‚ā˛ļā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąāŗā˛ĩā˛˛āŗā˛˛ā˛ŋ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", "local_asset_cast_failed": "ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛—āŗ† ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛Ļ ā˛¸āŗā˛ĩā˛¤āŗā˛¤ā˛¨āŗā˛¨āŗ ā˛Ŧā˛ŋā˛¤āŗā˛¤ā˛°ā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛", "local_network_sheet_info": "➍ā˛ŋā˛°āŗā˛Ļā˛ŋā˛ˇāŗā˛Ÿā˛Ēā˛Ąā˛ŋ➏ā˛ŋā˛Ļ ā˛ĩ⺈-ā˛Ģ⺈ ā˛¨āŗ†ā˛Ÿāŗâ€Œā˛ĩā˛°āŗā˛•āŗ ā˛Ŧ➺➏⺁ā˛ĩā˛žā˛— ➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ➈ URL ā˛Žāŗ‚ā˛˛ā˛• ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛—āŗ† ➏➂ā˛Ēā˛°āŗā˛•ā˛—āŗŠā˛ŗāŗā˛ŗāŗā˛¤āŗā˛¤ā˛Ļāŗ†", "location_permission_content": "ā˛¸āŗā˛ĩ➝➂-ā˛Ŧā˛Ļā˛˛ā˛žā˛ĩ➪⺆ ā˛ĩ⺈ā˛ļā˛ŋā˛ˇāŗā˛Ÿāŗā˛¯ā˛ĩā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏➞⺁, ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗâ€Œā˛—āŗ† ā˛Ēāŗā˛°ā˛¸āŗā˛¤āŗā˛¤ ā˛ĩ⺈-ā˛Ģ⺈ ā˛¨āŗ†ā˛Ÿāŗâ€Œā˛ĩā˛°āŗā˛•āŗâ€Œā˛¨ ā˛šāŗ†ā˛¸ā˛°ā˛¨āŗā˛¨āŗ ➓ā˛Ļ➞⺁ ➍ā˛ŋ➖➰ā˛ĩā˛žā˛Ļ ā˛¸āŗā˛Ĩ➺ ā˛…ā˛¨āŗā˛Žā˛¤ā˛ŋ➝ ā˛…ā˛—ā˛¤āŗā˛¯ā˛ĩā˛ŋā˛Ļāŗ†", + "location_picker_latitude_error": "ā˛Žā˛žā˛¨āŗā˛¯ā˛ĩā˛žā˛Ļ ā˛…ā˛•āŗā˛ˇā˛žā˛‚ā˛ļā˛ĩā˛¨āŗā˛¨āŗ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ", + "location_picker_latitude_hint": "➍ā˛ŋā˛Žāŗā˛Ž ā˛…ā˛•āŗā˛ˇā˛žā˛‚ā˛ļā˛ĩā˛¨āŗā˛¨āŗ ā˛‡ā˛˛āŗā˛˛ā˛ŋ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ", + "location_picker_longitude_error": "ā˛Žā˛žā˛¨āŗā˛¯ā˛ĩā˛žā˛Ļ ā˛°āŗ‡ā˛–ā˛žā˛‚ā˛ļā˛ĩā˛¨āŗā˛¨āŗ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ", + "location_picker_longitude_hint": "➍ā˛ŋā˛Žāŗā˛Ž ā˛°āŗ‡ā˛–ā˛žā˛‚ā˛ļā˛ĩā˛¨āŗā˛¨āŗ ā˛‡ā˛˛āŗā˛˛ā˛ŋ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ", "log_out_all_devices": "ā˛Žā˛˛āŗā˛˛ā˛ž ā˛¸ā˛žā˛§ā˛¨ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛˛ā˛žā˛—āŗ ā˛”ā˛Ÿāŗ ā˛Žā˛žā˛Ąā˛ŋ", "logged_out_all_devices": "ā˛Žā˛˛āŗā˛˛ā˛ž ā˛¸ā˛žā˛§ā˛¨ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛˛ā˛žā˛—āŗ ā˛”ā˛Ÿāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", "login": "ā˛˛ā˛žā˛—ā˛ŋā˛¨āŗ", + "login_disabled": "ā˛˛ā˛žā˛—ā˛ŋā˛¨āŗ ā˛…ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "login_form_api_exception": "API ā˛ĩā˛ŋā˛¨ā˛žā˛¯ā˛ŋ➤ā˛ŋ. ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ā˛¸ā˛°āŗā˛ĩā˛°āŗ URL ā˛…ā˛¨āŗā˛¨āŗ ā˛Ē➰ā˛ŋā˛ļ⺀➞ā˛ŋ➏ā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ ā˛Žā˛¤āŗā˛¤āŗ† ā˛Ēāŗā˛°ā˛¯ā˛¤āŗā˛¨ā˛ŋ➏ā˛ŋ.", "login_form_err_http": "ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ http:// ➅ā˛Ĩā˛ĩā˛ž https:// ā˛…ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛Ļā˛ŋā˛ˇāŗā˛Ÿā˛Ēā˛Ąā˛ŋ➏ā˛ŋ", "login_form_failed_get_oauth_server_config": "OAuth ā˛Ŧ➺➏ā˛ŋā˛•āŗŠā˛‚ā˛Ąāŗ ā˛˛ā˛žā˛—ā˛ŋā˛‚ā˛—āŗ ā˛Žā˛žā˛Ąāŗā˛ĩā˛žā˛— ā˛Ļ⺋➎, ā˛¸ā˛°āŗā˛ĩā˛°āŗ URL ā˛Ē➰ā˛ŋā˛ļ⺀➞ā˛ŋ➏ā˛ŋ", "login_form_failed_get_oauth_server_disable": "➈ ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ OAuth ā˛ĩ⺈ā˛ļā˛ŋā˛ˇāŗā˛Ÿāŗā˛¯ ā˛˛ā˛­āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛", + "login_form_failed_login": "➍ā˛ŋā˛Žāŗā˛Žā˛¨āŗā˛¨āŗ ā˛˛ā˛žā˛—ā˛ŋā˛¨āŗ ā˛Žā˛žā˛Ąāŗā˛ĩā˛˛āŗā˛˛ā˛ŋ ā˛Ļ⺋➎, ā˛¸ā˛°āŗā˛ĩā˛°āŗ URL, ā˛‡ā˛Žāŗ‡ā˛˛āŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛Ē➰ā˛ŋā˛ļ⺀➞ā˛ŋ➏ā˛ŋ", "login_form_handshake_exception": "ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛¨āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ā˛šāŗā˛¯ā˛žā˛‚ā˛Ąāŗâ€Œā˛ļāŗ‡ā˛•āŗ ā˛ĩā˛ŋā˛¨ā˛žā˛¯ā˛ŋ➤ā˛ŋ ā˛‡ā˛¤āŗā˛¤āŗ. ➍⺀ā˛ĩ⺁ ā˛¸āŗā˛ĩ➝➂ ā˛¸ā˛šā˛ŋ ā˛Žā˛žā˛Ąā˛ŋā˛Ļ ā˛Ēāŗā˛°ā˛Žā˛žā˛Ŗā˛Ēā˛¤āŗā˛°ā˛ĩā˛¨āŗā˛¨āŗ ā˛Ŧā˛ŗā˛¸āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗā˛Ļ➰⺆ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ ā˛¸āŗā˛ĩ➝➂ ā˛¸ā˛šā˛ŋ ā˛Žā˛žā˛Ąā˛ŋā˛Ļ ā˛Ēāŗā˛°ā˛Žā˛žā˛Ŗā˛Ēā˛¤āŗā˛° ā˛Ŧ⺆➂ā˛Ŧ➞ā˛ĩā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ.", + "login_form_server_empty": "ā˛¸ā˛°āŗā˛ĩā˛°āŗ URL ā˛…ā˛¨āŗā˛¨āŗ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ.", "login_form_server_error": "ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛—āŗ† ➏➂ā˛Ēā˛°āŗā˛•ā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛žā˛—ā˛˛ā˛ŋā˛˛āŗā˛˛.", "login_has_been_disabled": "ā˛˛ā˛žā˛—ā˛ŋā˛¨āŗ ā˛…ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†.", "login_password_changed_error": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛…ā˛¨āŗā˛¨āŗ ➍ā˛ĩ⺀➕➰ā˛ŋ➏⺁ā˛ĩā˛žā˛— ā˛Ļ⺋➎ ā˛•ā˛‚ā˛Ąāŗā˛Ŧ➂ā˛Ļā˛ŋā˛Ļāŗ†", + "logout_all_device_confirmation": "➍⺀ā˛ĩ⺁ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛¸ā˛žā˛§ā˛¨ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛˛ā˛žā˛—āŗ ā˛”ā˛Ÿāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", + "logout_this_device_confirmation": "➍⺀ā˛ĩ⺁ ➈ ā˛¸ā˛žā˛§ā˛¨ā˛ĩā˛¨āŗā˛¨āŗ ā˛˛ā˛žā˛—āŗ ā˛”ā˛Ÿāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", + "longitude": "ā˛°āŗ‡ā˛–ā˛žā˛‚ā˛ļ", + "look": "ā˛¨āŗ‹ā˛Ąā˛ŋ", + "loop_videos_description": "ā˛ĩā˛ŋā˛ĩ➰ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛•ā˛Ļā˛˛āŗā˛˛ā˛ŋ ā˛ĩāŗ€ā˛Ąā˛ŋ➝⺊ā˛ĩā˛¨āŗā˛¨āŗ ā˛¸āŗā˛ĩā˛¯ā˛‚ā˛šā˛žā˛˛ā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ➞⺂ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ.", "main_branch_warning": "➍⺀ā˛ĩ⺁ ➅➭ā˛ŋā˛ĩ⺃ā˛Ļāŗā˛§ā˛ŋ ➆ā˛ĩāŗƒā˛¤āŗā˛¤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛Ŧā˛ŗā˛¸āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗā˛Ļ⺀➰ā˛ŋ; ā˛Ŧā˛ŋā˛Ąāŗā˛—ā˛Ąāŗ† ➆ā˛ĩāŗƒā˛¤āŗā˛¤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏➞⺁ ā˛¨ā˛žā˛ĩ⺁ ā˛Ŧ➞ā˛ĩā˛žā˛—ā˛ŋ ā˛ļā˛ŋā˛Ģā˛žā˛°ā˛¸āŗ ā˛Žā˛žā˛Ąāŗā˛¤āŗā˛¤āŗ‡ā˛ĩāŗ†!", "maintenance_description": "ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ā˛…ā˛¨āŗā˛¨āŗ maintenance mode ā˛•āŗā˛•āŗ† ➇➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†.", "maintenance_end_error": "➍ā˛ŋā˛°āŗā˛ĩā˛šā˛Ŗā˛ž ā˛•āŗā˛°ā˛Žā˛ĩā˛¨āŗā˛¨āŗ ā˛•āŗŠā˛¨āŗ†ā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†.", @@ -939,56 +1102,97 @@ "manage_your_devices": "➍ā˛ŋā˛Žāŗā˛Ž ā˛˛ā˛žā˛—ā˛ŋā˛¨āŗ ➆➗ā˛ŋ➰⺁ā˛ĩ ā˛¸ā˛žā˛§ā˛¨ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", "manage_your_oauth_connection": "➍ā˛ŋā˛Žāŗā˛Ž OAuth ➏➂ā˛Ēā˛°āŗā˛•ā˛ĩā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", "map": "ā˛¨ā˛•āŗā˛ˇāŗ†", + "map_cannot_get_user_location": "ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛° ā˛¸āŗā˛Ĩ➺ā˛ĩā˛¨āŗā˛¨āŗ ā˛Ēā˛Ąāŗ†ā˛¯ā˛˛āŗ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛", "map_location_service_disabled_content": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ēāŗā˛°ā˛¸āŗā˛¤āŗā˛¤ ā˛¸āŗā˛Ĩ➺ā˛Ļā˛ŋ➂ā˛Ļ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛Ļā˛°āŗā˛ļā˛ŋ➏➞⺁ ā˛¸āŗā˛Ĩ➺ ➏⺇ā˛ĩāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏⺁ā˛ĩ ā˛…ā˛—ā˛¤āŗā˛¯ā˛ĩā˛ŋā˛Ļāŗ†. ➍⺀ā˛ĩ⺁ ā˛ˆā˛— ➅ā˛Ļā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", "map_marker_for_images": "{city}, {country} ā˛Ļā˛˛āŗā˛˛ā˛ŋ ➤⺆➗⺆ā˛Ļ ➚ā˛ŋā˛¤āŗā˛°ā˛—ā˛ŗā˛ŋā˛—ā˛žā˛—ā˛ŋ ā˛¨ā˛•āŗā˛ˇāŗ† ā˛Žā˛žā˛°āŗā˛•ā˛°āŗ", "map_marker_with_image": "➚ā˛ŋā˛¤āŗā˛°ā˛ĻāŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ā˛¨ā˛•āŗā˛ˇāŗ† ā˛Žā˛žā˛°āŗā˛•ā˛°āŗ", "map_no_location_permission_content": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ēāŗā˛°ā˛¸āŗā˛¤āŗā˛¤ ā˛¸āŗā˛Ĩ➺ā˛Ļā˛ŋ➂ā˛Ļ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛Ļā˛°āŗā˛ļā˛ŋ➏➞⺁ ā˛¸āŗā˛Ĩ➺ ā˛…ā˛¨āŗā˛Žā˛¤ā˛ŋ ā˛…ā˛—ā˛¤āŗā˛¯ā˛ĩā˛ŋā˛Ļāŗ†. ➍⺀ā˛ĩ⺁ ā˛ˆā˛— ➅ā˛Ļā˛¨āŗā˛¨āŗ ā˛…ā˛¨āŗā˛Žā˛¤ā˛ŋ➏➞⺁ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", "map_zoom_to_see_photos": "ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¨āŗ‹ā˛Ąā˛˛āŗ ā˛āŗ‚ā˛Žāŗ ā˛”ā˛Ÿāŗ ā˛Žā˛žā˛Ąā˛ŋ", + "matches": "ā˛Ē➂ā˛Ļāŗā˛¯ā˛—ā˛ŗāŗ", "memories": "➍⺆➍ā˛Ē⺁➗➺⺁", "memories_check_back_tomorrow": "ā˛šāŗ†ā˛šāŗā˛šā˛ŋ➍ ➍⺆➍ā˛Ē⺁➗➺ā˛ŋā˛—ā˛žā˛—ā˛ŋ ā˛¨ā˛žā˛ŗāŗ† ā˛Žā˛¤āŗā˛¤āŗ† ā˛Ē➰ā˛ŋā˛ļ⺀➞ā˛ŋ➏ā˛ŋ", "memories_setting_description": "➍ā˛ŋā˛Žāŗā˛Ž ➍⺆➍ā˛Ēāŗā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ ➍⺀ā˛ĩ⺁ ā˛¨āŗ‹ā˛Ąāŗā˛ĩ⺁ā˛Ļā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", + "memories_swipe_to_close": "ā˛Žāŗā˛šāŗā˛šā˛˛āŗ ā˛Žāŗ‡ā˛˛ā˛•āŗā˛•āŗ† ā˛¸āŗā˛ĩ⺈ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛ŋ", "memory": "➍⺆➍ā˛Ē⺁", + "menu": "ā˛Žāŗ†ā˛¨āŗ", + "merge": "ā˛ĩā˛ŋ➞⺀➍", "merge_people_limit": "➍⺀ā˛ĩ⺁ ā˛’ā˛Žāŗā˛Žāŗ†ā˛—āŗ† 5 ā˛Žāŗā˛–ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Žā˛žā˛¤āŗā˛° ā˛ĩā˛ŋā˛˛āŗ€ā˛¨ā˛—āŗŠā˛ŗā˛ŋ➏ā˛Ŧā˛šāŗā˛Ļ⺁", "merge_people_prompt": "➍⺀ā˛ĩ⺁ ➈ ā˛œā˛¨ā˛°ā˛¨āŗā˛¨āŗ ā˛ĩā˛ŋā˛˛āŗ€ā˛¨ā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž? ➈ ā˛•āŗā˛°ā˛ŋā˛¯āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛ĩ⺁ā˛Ļā˛ŋā˛˛āŗā˛˛.", + "minimize": "➕➍ā˛ŋā˛ˇāŗā˛ āŗ€ā˛•ā˛°ā˛ŋ➏ā˛ŋ", + "minute": "➍ā˛ŋā˛Žā˛ŋ➎", + "missing": "ā˛•ā˛žā˛Ŗāŗ†ā˛¯ā˛žā˛—ā˛ŋā˛Ļāŗ†", "mobile_app_download_onboarding_note": "➈ ➕⺆➺➗ā˛ŋ➍ ā˛†ā˛¯āŗā˛•āŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏ā˛ŋā˛•āŗŠā˛‚ā˛Ąāŗ ➕➂ā˛Ēāŗā˛¯ā˛žā˛¨ā˛ŋā˛¯ā˛¨āŗ ā˛ŽāŗŠā˛Ŧāŗˆā˛˛āŗ ➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛ĄāŗŒā˛¨āŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛ŋ", + "model": "ā˛Žā˛žā˛Ļ➰ā˛ŋ", + "month": "➤ā˛ŋ➂➗➺⺁", + "more": "ā˛‡ā˛¨āŗā˛¨ā˛ˇāŗā˛Ÿāŗ", "move_off_locked_folder": "ā˛˛ā˛žā˛•āŗ ā˛Žā˛žā˛Ąā˛ŋā˛Ļ ā˛Ģāŗ‹ā˛˛āŗā˛Ąā˛°āŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ ā˛šāŗŠā˛°ā˛—āŗ† ➏➰ā˛ŋ➏ā˛ŋ", "move_to_lock_folder_action_prompt": "ā˛˛ā˛žā˛•āŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛Ļ ā˛Ģāŗ‹ā˛˛āŗā˛Ąā˛°āŗâ€Œā˛—āŗ† {count} ➏⺇➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", "move_to_locked_folder_confirmation": "➈ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—ā˛ŗā˛ŋ➂ā˛Ļ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ† ā˛Žā˛¤āŗā˛¤āŗ ā˛˛ā˛žā˛•āŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛Ļ ā˛Ģāŗ‹ā˛˛āŗā˛Ąā˛°āŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ ā˛Žā˛žā˛¤āŗā˛° ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛Ŧā˛šāŗā˛Ļā˛žā˛—ā˛ŋā˛Ļāŗ†", "multiselect_grid_edit_date_time_err_read_only": "➓ā˛Ļ➞⺁ ā˛Žā˛žā˛¤āŗā˛° ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗ(➗➺) ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛•ā˛ĩā˛¨āŗā˛¨āŗ ➏➂ā˛Ēā˛žā˛Ļā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛, ā˛Ŧā˛ŋā˛Ÿāŗā˛Ÿāŗā˛Ŧā˛ŋā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", "multiselect_grid_edit_gps_err_read_only": "➓ā˛Ļ➞⺁ ā˛Žā˛žā˛¤āŗā˛° ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗ(➗➺) ā˛¸āŗā˛Ĩ➺ā˛ĩā˛¨āŗā˛¨āŗ ➏➂ā˛Ēā˛žā˛Ļā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛, ā˛Ŧā˛ŋā˛Ÿāŗā˛Ÿāŗā˛Ŧā˛ŋā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", + "name": "ā˛šāŗ†ā˛¸ā˛°āŗ", "network_requirement_photos_upload": "ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛¸āŗ†ā˛˛āŗā˛¯āŗā˛˛ā˛žā˛°āŗ ā˛Ąāŗ‡ā˛Ÿā˛žā˛ĩā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏ā˛ŋ", "network_requirement_videos_upload": "ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛¸āŗ†ā˛˛āŗā˛¯āŗā˛˛ā˛žā˛°āŗ ā˛Ąāŗ‡ā˛Ÿā˛žā˛ĩā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏ā˛ŋ", "network_requirements_updated": "ā˛¨āŗ†ā˛Ÿāŗâ€Œā˛ĩā˛°āŗā˛•āŗ ➅ā˛ĩā˛ļāŗā˛¯ā˛•ā˛¤āŗ†ā˛—ā˛ŗāŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛—ā˛ŋā˛ĩāŗ†, ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛•āŗā˛¯āŗ‚ ā˛…ā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗā˛šāŗŠā˛‚ā˛Ļā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", "networking_subtitle": "ā˛¸ā˛°āŗā˛ĩā˛°āŗ ā˛Žā˛‚ā˛Ąāŗâ€Œā˛Ēā˛žā˛¯ā˛ŋā˛‚ā˛Ÿāŗ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", "new_pin_code_subtitle": "ā˛˛ā˛žā˛•āŗ ā˛Žā˛žā˛Ąā˛ŋā˛Ļ ā˛Ģāŗ‹ā˛˛āŗā˛Ąā˛°āŗ ā˛…ā˛¨āŗā˛¨āŗ ➍⺀ā˛ĩ⺁ ā˛ŽāŗŠā˛Ļ➞ ā˛Ŧā˛žā˛°ā˛ŋ➗⺆ ā˛Ēāŗā˛°ā˛ĩāŗ‡ā˛ļā˛ŋā˛¸āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗā˛Ļ⺀➰ā˛ŋ. ➈ ā˛Ē⺁➟ā˛ĩā˛¨āŗā˛¨āŗ ā˛¸āŗā˛°ā˛•āŗā˛ˇā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ēāŗā˛°ā˛ĩāŗ‡ā˛ļā˛ŋ➏➞⺁ ā˛Ēā˛ŋā˛¨āŗ ā˛•āŗ‹ā˛Ąāŗ ➰➚ā˛ŋ➏ā˛ŋ", + "no": "ā˛‡ā˛˛āŗā˛˛", + "no_albums_message": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¸ā˛‚ā˛˜ā˛Ÿā˛ŋ➏➞⺁ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ➰➚ā˛ŋ➏ā˛ŋ", "no_albums_with_name_yet": "➈ ā˛šāŗ†ā˛¸ā˛°ā˛ŋā˛¨āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ➍⺀ā˛ĩ⺁ ā˛‡ā˛¨āŗā˛¨āŗ‚ ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛šāŗŠā˛‚ā˛Ļā˛ŋā˛˛āŗā˛˛ ā˛Žā˛‚ā˛Ļ⺁ ā˛¤āŗ‹ā˛°āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†.", + "no_albums_yet": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ŧ➺ā˛ŋ ā˛‡ā˛¨āŗā˛¨āŗ‚ ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—ā˛ŗā˛ŋā˛˛āŗā˛˛ ā˛Žā˛‚ā˛Ļ⺁ ā˛¤āŗ‹ā˛°āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†.", "no_archived_assets_message": "➍ā˛ŋā˛Žāŗā˛Ž Photos ā˛ĩāŗ€ā˛•āŗā˛ˇā˛Ŗāŗ†ā˛¯ā˛ŋ➂ā˛Ļ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗ†ā˛Žā˛žā˛Ąā˛˛āŗ ➅ā˛ĩāŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛†ā˛°āŗā˛•āŗˆā˛ĩāŗ ā˛Žā˛žā˛Ąā˛ŋ", "no_assets_message": "➍ā˛ŋā˛Žāŗā˛Ž ā˛ŽāŗŠā˛Ļ➞ ā˛Ģāŗ‹ā˛Ÿāŗ‹ ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛•āŗā˛˛ā˛ŋā˛•āŗ ā˛Žā˛žā˛Ąā˛ŋ", + "no_assets_to_show": "➤⺋➰ā˛ŋ➏➞⺁ ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛ŋā˛˛āŗā˛˛", "no_checksum_local": "ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛šāŗ†ā˛•āŗā˛¸ā˛Žāŗ ā˛˛ā˛­āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛ - ā˛¸āŗā˛Ĩ➺⺀➝ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ēā˛Ąāŗ†ā˛¯ā˛˛āŗ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛", "no_checksum_remote": "ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛šāŗ†ā˛•āŗā˛¸ā˛Žāŗ ā˛˛ā˛­āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛ - ➰ā˛ŋā˛Žāŗ‹ā˛Ÿāŗ ā˛†ā˛¸āŗā˛¤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛Ēā˛Ąāŗ†ā˛¯ā˛˛āŗ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛", "no_duplicates_found": "ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ➍➕➞⺁➗➺⺁ ā˛•ā˛‚ā˛Ąāŗā˛Ŧ➂ā˛Ļā˛ŋā˛˛āŗā˛˛.", "no_exif_info_available": "ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛Žā˛•āŗā˛¸ā˛ŋā˛Ģāŗ ā˛Žā˛žā˛šā˛ŋ➤ā˛ŋ ā˛˛ā˛­āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛", "no_explore_results_message": "➍ā˛ŋā˛Žāŗā˛Ž ā˛¸ā˛‚ā˛—āŗā˛°ā˛šā˛ĩā˛¨āŗā˛¨āŗ ā˛…ā˛¨āŗā˛ĩ⺇➎ā˛ŋ➏➞⺁ ā˛šāŗ†ā˛šāŗā˛šā˛ŋ➍ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛ŋ.", + "no_favorites_message": "➍ā˛ŋā˛Žāŗā˛Ž ā˛…ā˛¤āŗā˛¯āŗā˛¤āŗā˛¤ā˛Ž ➚ā˛ŋā˛¤āŗā˛°ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¤āŗā˛ĩ➰ā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛šāŗā˛Ąāŗā˛•ā˛˛āŗ ā˛Žāŗ†ā˛šāŗā˛šā˛ŋ➍ā˛ĩāŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏ā˛ŋ", + "no_libraries_message": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏➞⺁ ā˛Ŧā˛žā˛šāŗā˛¯ ➞⺈ā˛Ŧāŗā˛°ā˛°ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➰➚ā˛ŋ➏ā˛ŋ", "no_local_assets_found": "➈ ā˛šāŗ†ā˛•āŗā˛¸ā˛Žāŗâ€Œā˛¨āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛¸āŗā˛Ĩ➺⺀➝ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗāŗ ā˛•ā˛‚ā˛Ąāŗā˛Ŧ➂ā˛Ļā˛ŋā˛˛āŗā˛˛", "no_locked_photos_message": "ā˛˛ā˛žā˛•āŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛Ļ ā˛Ģāŗ‹ā˛˛āŗā˛Ąā˛°āŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ➰⺁ā˛ĩ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗ†ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ† ā˛Žā˛¤āŗā˛¤āŗ ➍⺀ā˛ĩ⺁ ➍ā˛ŋā˛Žāŗā˛Ž ➞⺈ā˛Ŧāŗā˛°ā˛°ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛Ŧāŗā˛°āŗŒā˛¸āŗ ā˛Žā˛žā˛Ąāŗā˛ĩā˛žā˛— ➅ā˛Ĩā˛ĩā˛ž ā˛šāŗā˛Ąāŗā˛•āŗā˛ĩā˛žā˛— ➅ā˛ĩ⺁ ā˛•ā˛žā˛Ŗā˛ŋ➏⺁ā˛ĩ⺁ā˛Ļā˛ŋā˛˛āŗā˛˛.", "no_remote_assets_found": "➈ ā˛šāŗ†ā˛•āŗā˛¸ā˛Žāŗâ€Œā˛¨āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ➰ā˛ŋā˛Žāŗ‹ā˛Ÿāŗ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗāŗ ā˛•ā˛‚ā˛Ąāŗā˛Ŧ➂ā˛Ļā˛ŋā˛˛āŗā˛˛", "no_results_description": "ā˛¸ā˛Žā˛žā˛¨ā˛žā˛°āŗā˛Ĩ➕ ā˛Ēā˛Ļ ➅ā˛Ĩā˛ĩā˛ž ā˛šāŗ†ā˛šāŗā˛šāŗ ā˛¸ā˛žā˛Žā˛žā˛¨āŗā˛¯ ➕⺀ā˛ĩā˛°āŗā˛Ąāŗ ā˛Ēāŗā˛°ā˛¯ā˛¤āŗā˛¨ā˛ŋ➏ā˛ŋ", "no_shared_albums_message": "➍ā˛ŋā˛Žāŗā˛Ž ā˛¨āŗ†ā˛Ÿāŗâ€Œā˛ĩā˛°āŗā˛•āŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ➰⺁ā˛ĩ ā˛œā˛¨ā˛°āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛˛āŗ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ➰➚ā˛ŋ➏ā˛ŋ", "not_in_any_album": "ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋā˛˛āŗā˛˛", + "notes": "➟ā˛ŋā˛Ēāŗā˛Ē➪ā˛ŋ➗➺⺁", "notification_permission_dialog_content": "➅➧ā˛ŋā˛¸āŗ‚ā˛šā˛¨āŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏➞⺁, ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛ŋ➗⺆ ā˛šāŗ‹ā˛—ā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ ā˛…ā˛¨āŗā˛Žā˛¤ā˛ŋ➏⺁ ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋ.", "notification_permission_list_tile_content": "➅➧ā˛ŋā˛¸āŗ‚ā˛šā˛¨āŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛…ā˛¨āŗā˛Žā˛¤ā˛ŋ ā˛¨āŗ€ā˛Ąā˛ŋ.", + "notifications": "➅➧ā˛ŋā˛¸āŗ‚ā˛šā˛¨āŗ†ā˛—ā˛ŗāŗ", "obtainium_configurator_instructions": "ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ➗ā˛ŋā˛Ÿāŗâ€Œā˛šā˛Ŧāŗâ€Œā˛¨ ā˛Ŧā˛ŋā˛Ąāŗā˛—ā˛Ąāŗ†ā˛¯ā˛ŋ➂ā˛Ļ ➍⺇➰ā˛ĩā˛žā˛—ā˛ŋ ā˛†ā˛‚ā˛Ąāŗā˛°ā˛žā˛¯āŗā˛Ąāŗ ➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛¸āŗā˛Ĩā˛žā˛Ēā˛ŋ➏➞⺁ ā˛Žā˛¤āŗā˛¤āŗ ➍ā˛ĩ⺀➕➰ā˛ŋ➏➞⺁ ā˛…ā˛Ÿāŗ‡ā˛Ÿā˛ŋ➍ā˛ŋā˛¯ā˛Žāŗ ā˛Ŧ➺➏ā˛ŋ. API ➕⺀➞ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➰➚ā˛ŋ➏ā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ ➍ā˛ŋā˛Žāŗā˛Ž ā˛…ā˛Ÿāŗ‡ā˛Ÿā˛ŋ➍ā˛ŋā˛¯ā˛Žāŗ ā˛•ā˛žā˛¨āŗā˛Ģā˛ŋ➗➰⺇ā˛ļā˛¨āŗ ➞ā˛ŋā˛‚ā˛•āŗ ā˛…ā˛¨āŗā˛¨āŗ ➰➚ā˛ŋ➏➞⺁ ➰⺂ā˛Ēā˛žā˛‚ā˛¤ā˛°ā˛ĩā˛¨āŗā˛¨āŗ ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋ", + "offline": "➆ā˛Ģāŗ ā˛˛āŗˆā˛¨āŗ", + "ok": "➏➰ā˛ŋ", + "onboarding": "ā˛†ā˛¨āŗ ā˛Ŧāŗ‹ā˛°āŗā˛Ąā˛ŋā˛‚ā˛—āŗ", "onboarding_locale_description": "➍ā˛ŋā˛Žāŗā˛Ž ➆ā˛Ļāŗā˛¯ā˛¤āŗ†ā˛¯ ā˛­ā˛žā˛ˇāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋ. ➍⺀ā˛ĩ⺁ ➇ā˛Ļā˛¨āŗā˛¨āŗ ➍➂➤➰ ➍ā˛ŋā˛Žāŗā˛Ž ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏ā˛Ŧā˛šāŗā˛Ļ⺁.", "onboarding_privacy_description": "➕⺆➺➗ā˛ŋ➍ (ā˛ā˛šāŗā˛›ā˛ŋ➕) ā˛ĩ⺈ā˛ļā˛ŋā˛ˇāŗā˛Ÿāŗā˛¯ā˛—ā˛ŗāŗ ā˛Ŧā˛žā˛šāŗā˛¯ ➏⺇ā˛ĩāŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➅ā˛ĩ➞➂ā˛Ŧā˛ŋ➏ā˛ŋā˛ĩāŗ† ā˛Žā˛¤āŗā˛¤āŗ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛¸ā˛Žā˛¯ā˛Ļā˛˛āŗā˛˛ā˛ŋ ➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛Ŧā˛šāŗā˛Ļ⺁.", + "onboarding_server_welcome_description": "➍ā˛ŋā˛Žāŗā˛Ž ➍ā˛ŋā˛Ļā˛°āŗā˛ļ➍ā˛ĩā˛¨āŗā˛¨āŗ ➕⺆➞ā˛ĩ⺁ ā˛¸ā˛žā˛Žā˛žā˛¨āŗā˛¯ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ā˛šāŗŠā˛‚ā˛Ļā˛ŋ➏⺋➪.", "onboarding_theme_description": "➍ā˛ŋā˛Žāŗā˛Ž ➍ā˛ŋā˛Ļā˛°āŗā˛ļā˛¨ā˛•āŗā˛•āŗ† ā˛Ŧā˛Ŗāŗā˛Ŗā˛Ļ ā˛Ĩāŗ€ā˛Žāŗ ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋ. ➍⺀ā˛ĩ⺁ ➇ā˛Ļā˛¨āŗā˛¨āŗ ➍➂➤➰ ➍ā˛ŋā˛Žāŗā˛Ž ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏ā˛Ŧā˛šāŗā˛Ļ⺁.", + "online": "ā˛†ā˛¨āŗ ā˛˛āŗˆā˛¨āŗ", "open_in_map_view": "ā˛¨ā˛•āŗā˛ˇāŗ† ā˛ĩāŗ€ā˛•āŗā˛ˇā˛Ŗāŗ†ā˛¯ā˛˛āŗā˛˛ā˛ŋ ➤⺆➰⺆➝ā˛ŋ➰ā˛ŋ", "open_the_search_filters": "ā˛šāŗā˛Ąāŗā˛•ā˛žā˛Ÿ ā˛Ģā˛ŋā˛˛āŗā˛Ÿā˛°āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➤⺆➰⺆➝ā˛ŋ➰ā˛ŋ", + "options": "ā˛†ā˛¯āŗā˛•āŗ†ā˛—ā˛ŗāŗ", + "or": "➅ā˛Ĩā˛ĩā˛ž", "organize_into_albums_description": "ā˛Ēāŗā˛°ā˛¸āŗā˛¤āŗā˛¤ ➏ā˛ŋā˛‚ā˛•āŗ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏ā˛ŋā˛•āŗŠā˛‚ā˛Ąāŗ ā˛…ā˛¸āŗā˛¤ā˛ŋā˛¤āŗā˛ĩā˛Ļā˛˛āŗā˛˛ā˛ŋ➰⺁ā˛ĩ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ ➇➰ā˛ŋ➏ā˛ŋ", + "original": "ā˛Žāŗ‚ā˛˛", + "other": "➇➤➰", + "owned": "ā˛Žā˛žā˛˛āŗ€ā˛•ā˛¤āŗā˛ĩ", + "owner": "ā˛Žā˛žā˛˛āŗ€ā˛•", + "partner": "ā˛Ēā˛žā˛˛āŗā˛Ļā˛žā˛°", "partner_can_access_assets": "ā˛†ā˛°āŗā˛•āŗˆā˛ĩāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛Ļ ā˛Žā˛¤āŗā˛¤āŗ ➅➺ā˛ŋā˛¸ā˛˛ā˛žā˛Ļ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛šāŗŠā˛°ā˛¤āŗā˛Ēā˛Ąā˛ŋ➏ā˛ŋ ➍ā˛ŋā˛Žāŗā˛Ž ā˛Žā˛˛āŗā˛˛ā˛ž ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗāŗ", "partner_can_access_location": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļ ā˛¸āŗā˛Ĩ➺", "partner_page_empty_message": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛‡ā˛¨āŗā˛¨āŗ‚ ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛Ēā˛žā˛˛āŗā˛Ļā˛žā˛°ā˛°āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛‚ā˛Ąā˛ŋā˛˛āŗā˛˛.", "partner_page_no_more_users": "➏⺇➰ā˛ŋ➏➞⺁ ā˛‡ā˛¨āŗā˛¨āŗ ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛ŋā˛˛āŗā˛˛", + "partner_page_partner_add_failed": "ā˛Ēā˛žā˛˛āŗā˛Ļā˛žā˛°ā˛°ā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏➞⺁ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", + "partner_page_stop_sharing_content": "{partner} ā˛‡ā˛¨āŗā˛¨āŗ ā˛Žāŗā˛‚ā˛Ļāŗ† ➍ā˛ŋā˛Žāŗā˛Ž ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛ĩāŗ‡ā˛ļā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛žā˛—āŗā˛ĩ⺁ā˛Ļā˛ŋā˛˛āŗā˛˛.", + "partners": "ā˛Ēā˛žā˛˛āŗā˛Ļā˛žā˛°ā˛°āŗ", + "password": "ā˛Ēā˛žā˛¸āŗā˛ĩā˛°āŗā˛Ąāŗ", "password_does_not_match": "ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛šāŗŠā˛‚ā˛Ļā˛ŋā˛•āŗ†ā˛¯ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛˛āŗā˛˛", + "path": "ā˛šā˛žā˛Ļā˛ŋ", + "pattern": "ā˛Ēāŗā˛¯ā˛žā˛Ÿā˛°āŗā˛¨āŗ", + "pause": "ā˛ĩā˛ŋā˛°ā˛žā˛Ž", + "pending": "ā˛Ŧā˛žā˛•ā˛ŋ ➉➺ā˛ŋā˛Ļā˛ŋā˛Ļāŗ†", + "people": "➜➍➰⺁", "people_feature_description": "➜➍➰ā˛ŋ➂ā˛Ļ ➗⺁➂ā˛Ē⺁ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛Ļ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ŧāŗā˛°āŗŒā˛¸āŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", "people_sidebar_description": "ā˛¸āŗˆā˛Ąāŗâ€Œā˛Ŧā˛žā˛°āŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ ➜➍➰⺁ ā˛Žā˛‚ā˛Ŧ ➞ā˛ŋā˛‚ā˛•āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛Ļā˛°āŗā˛ļā˛ŋ➏ā˛ŋ", "permanent_deletion_warning_setting_description": "ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛ļā˛žā˛ļāŗā˛ĩ➤ā˛ĩā˛žā˛—ā˛ŋ ➅➺ā˛ŋ➏⺁ā˛ĩā˛žā˛— ā˛Žā˛šāŗā˛šā˛°ā˛ŋā˛•āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➤⺋➰ā˛ŋ➏ā˛ŋ", @@ -998,27 +1202,52 @@ "permission_onboarding_permission_granted": "ā˛…ā˛¨āŗā˛Žā˛¤ā˛ŋ ā˛¨āŗ€ā˛Ąā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†! ➍⺀ā˛ĩ⺁ ➏ā˛ŋā˛Ļāŗā˛§ā˛°ā˛žā˛—ā˛ŋā˛Ļāŗā˛Ļ⺀➰ā˛ŋ.", "permission_onboarding_permission_limited": "ā˛…ā˛¨āŗā˛Žā˛¤ā˛ŋ ā˛¸āŗ€ā˛Žā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†. ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ➍ā˛ŋā˛Žāŗā˛Ž ➏➂ā˛Ēāŗ‚ā˛°āŗā˛Ŗ ā˛—āŗā˛¯ā˛žā˛˛ā˛°ā˛ŋ ā˛¸ā˛‚ā˛—āŗā˛°ā˛šā˛ĩā˛¨āŗā˛¨āŗ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛Žā˛¤āŗā˛¤āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏➞⺁, ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ ā˛Ģāŗ‹ā˛Ÿāŗ‹ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋ➝⺊ ā˛…ā˛¨āŗā˛Žā˛¤ā˛ŋā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¨āŗ€ā˛Ąā˛ŋ.", "permission_onboarding_request": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏➞⺁ ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗâ€Œā˛—āŗ† ā˛…ā˛¨āŗā˛Žā˛¤ā˛ŋ ā˛Ŧ⺇➕⺁.", + "person": "ā˛ĩāŗā˛¯ā˛•āŗā˛¤ā˛ŋ", "photo_shared_all_users": "➍⺀ā˛ĩ⺁ ➍ā˛ŋā˛Žāŗā˛Ž ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛‚ā˛Ąā˛ŋ➰⺁ā˛ĩ➂➤⺆ ā˛•ā˛žā˛Ŗāŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ† ➅ā˛Ĩā˛ĩā˛ž ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛˛āŗ ➍ā˛ŋā˛Žāŗā˛Ž ā˛Ŧ➺ā˛ŋ ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛ŋā˛˛āŗā˛˛.", + "photos": "ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ", "photos_from_previous_years": "ā˛šā˛ŋ➂ā˛Ļā˛ŋ➍ ā˛ĩā˛°āŗā˛ˇā˛—ā˛ŗ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ", "pin_code_setup_successfully": "ā˛Ēā˛ŋā˛¨āŗ ā˛•āŗ‹ā˛Ąāŗ ā˛…ā˛¨āŗā˛¨āŗ ➝ā˛ļā˛¸āŗā˛ĩā˛ŋā˛¯ā˛žā˛—ā˛ŋ ā˛šāŗŠā˛‚ā˛Ļā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "place": "ā˛¸āŗā˛Ĩ➺", + "places": "ā˛¸āŗā˛Ĩ➺➗➺⺁", + "play": "ā˛Ēāŗā˛˛āŗ‡ ā˛Žā˛žā˛Ąā˛ŋ", "play_or_pause_video": "ā˛ĩāŗ€ā˛Ąā˛ŋ➝⺊ ā˛Ēāŗā˛˛āŗ‡ ā˛Žā˛žā˛Ąā˛ŋ ➅ā˛Ĩā˛ĩā˛ž ā˛ĩā˛ŋā˛°ā˛žā˛Žā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", "play_original_video_setting_description": "ā˛Ÿāŗā˛°ā˛žā˛¨āŗā˛¸āŗâ€Œā˛•āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛ŋā˛Ļ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛ŋ➗ā˛ŋ➂➤ ā˛Žāŗ‚ā˛˛ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗ ā˛Ēāŗā˛˛āŗ‡ā˛Ŧāŗā˛¯ā˛žā˛•āŗâ€Œā˛—āŗ† ➆ā˛Ļāŗā˛¯ā˛¤āŗ† ā˛¨āŗ€ā˛Ąā˛ŋ. ā˛Žāŗ‚ā˛˛ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗ ā˛šāŗŠā˛‚ā˛Ļā˛žā˛Ŗā˛ŋā˛•āŗ†ā˛¯ā˛žā˛—ā˛Ļā˛ŋā˛Ļāŗā˛Ļ➰⺆ ➅ā˛Ļ⺁ ➏➰ā˛ŋā˛¯ā˛žā˛—ā˛ŋ ā˛Ēāŗā˛˛āŗ‡ā˛Ŧāŗā˛¯ā˛žā˛•āŗ ➆➗ā˛Ļā˛ŋ➰ā˛Ŧā˛šāŗā˛Ļ⺁.", + "port": "ā˛Ēāŗ‹ā˛°āŗā˛Ÿāŗ", + "preferences_settings_subtitle": "➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗâ€Œā˛¨ ➆ā˛Ļāŗā˛¯ā˛¤āŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", + "preset": "ā˛ŽāŗŠā˛Ļ➞⺇", + "preview": "ā˛Ēāŗ‚ā˛°āŗā˛ĩā˛ĩāŗ€ā˛•āŗā˛ˇā˛Ŗāŗ†", + "previous": "ā˛šā˛ŋ➂ā˛Ļā˛ŋ➍", + "primary": "ā˛Ēāŗā˛°ā˛žā˛Ĩā˛Žā˛ŋ➕", + "privacy": "ā˛—āŗŒā˛Ēāŗā˛¯ā˛¤āŗ†", "profile_drawer_client_server_up_to_date": "ā˛•āŗā˛˛āŗˆā˛‚ā˛Ÿāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛¸ā˛°āŗā˛ĩā˛°āŗ ➍ā˛ĩāŗ€ā˛•āŗƒā˛¤ā˛ĩā˛žā˛—ā˛ŋā˛ĩāŗ†", + "profile_drawer_readonly_mode": "➓ā˛Ļ➞⺁-ā˛Žā˛žā˛¤āŗā˛° ā˛Žāŗ‹ā˛Ąāŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†. ➍ā˛ŋā˛°āŗā˛—ā˛Žā˛ŋ➏➞⺁ ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛° ➅ā˛ĩā˛¤ā˛žā˛°āŗ ā˛ā˛•ā˛žā˛¨āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ļāŗ€ā˛°āŗā˛˜ā˛•ā˛žā˛˛ ā˛’ā˛¤āŗā˛¤ā˛ŋ➰ā˛ŋ.", "profile_image_of_user": "{user} ➰ ā˛Ēāŗā˛°āŗŠā˛Ģāŗˆā˛˛āŗ ➚ā˛ŋā˛¤āŗā˛°", + "purchase_account_info": "ā˛Ŧ⺆➂ā˛Ŧ➞ā˛ŋ➗", "purchase_activated_subtitle": "ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ā˛Žā˛¤āŗā˛¤āŗ ➓ā˛Ēā˛¨āŗ ā˛¸āŗ‹ā˛°āŗā˛¸āŗ ā˛¸ā˛žā˛Ģāŗā˛Ÿāŗâ€Œā˛ĩāŗ‡ā˛°āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ŧ⺆➂ā˛Ŧ➞ā˛ŋ➏ā˛ŋā˛Ļāŗā˛Ļā˛•āŗā˛•ā˛žā˛—ā˛ŋ ā˛§ā˛¨āŗā˛¯ā˛ĩā˛žā˛Ļ➗➺⺁", "purchase_activated_title": "➍ā˛ŋā˛Žāŗā˛Ž ➕⺀➞ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➝ā˛ļā˛¸āŗā˛ĩā˛ŋā˛¯ā˛žā˛—ā˛ŋ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "purchase_button_buy": "➖➰⺀ā˛Ļā˛ŋ➏ā˛ŋ", "purchase_button_reminder": "30 ā˛Ļā˛ŋā˛¨ā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ ➍➍➗⺆ ➍⺆➍ā˛Ēā˛ŋ➏ā˛ŋ", + "purchase_button_select": "ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋ", "purchase_failed_activation": "ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†! ➏➰ā˛ŋā˛¯ā˛žā˛Ļ ā˛‰ā˛¤āŗā˛Ēā˛¨āŗā˛¨ ➕⺀➞ā˛ŋā˛—ā˛žā˛—ā˛ŋ ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ➍ā˛ŋā˛Žāŗā˛Ž ā˛‡ā˛Žāŗ‡ā˛˛āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ē➰ā˛ŋā˛ļ⺀➞ā˛ŋ➏ā˛ŋ!", + "purchase_individual_title": "ā˛ĩāŗˆā˛¯ā˛•āŗā˛¤ā˛ŋ➕", "purchase_input_suggestion": "ā˛‰ā˛¤āŗā˛Ēā˛¨āŗā˛¨ ➕⺀➞ā˛ŋ ➇ā˛Ļ⺆➝⺇? ➕⺆➺➗⺆ ➕⺀➞ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ", + "purchase_license_subtitle": "➏⺇ā˛ĩ⺆➝ ➍ā˛ŋ➰➂➤➰ ➅➭ā˛ŋā˛ĩ⺃ā˛Ļāŗā˛§ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛Ŧ⺆➂ā˛Ŧ➞ā˛ŋ➏➞⺁ ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ā˛…ā˛¨āŗā˛¨āŗ ➖➰⺀ā˛Ļā˛ŋ➏ā˛ŋ", "purchase_panel_info_1": "ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ➍ā˛ŋā˛°āŗā˛Žā˛žā˛Ŗā˛ĩ⺁ ā˛¸ā˛žā˛•ā˛ˇāŗā˛Ÿāŗ ā˛¸ā˛Žā˛¯ ā˛Žā˛¤āŗā˛¤āŗ ā˛ļāŗā˛°ā˛Žā˛ĩā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļāŗā˛•āŗŠā˛ŗāŗā˛ŗāŗā˛¤āŗā˛¤ā˛Ļāŗ†, ā˛Žā˛¤āŗā˛¤āŗ ➅ā˛Ļā˛¨āŗā˛¨āŗ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛žā˛Ļā˛ˇāŗā˛Ÿāŗ ā˛‰ā˛¤āŗā˛¤ā˛Žā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛¨ā˛žā˛ĩ⺁ ā˛Ēāŗ‚ā˛°āŗā˛Ŗ ā˛¸ā˛Žā˛¯ā˛Ļ ā˛Žā˛‚ā˛œā˛ŋ➍ā˛ŋā˛¯ā˛°āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛šāŗŠā˛‚ā˛Ļā˛ŋā˛Ļāŗā˛Ļāŗ‡ā˛ĩāŗ†. ➓ā˛Ēā˛¨āŗ-ā˛¸āŗ‹ā˛°āŗā˛¸āŗ ā˛¸ā˛žā˛Ģāŗā˛Ÿāŗâ€Œā˛ĩāŗ‡ā˛°āŗ ā˛Žā˛¤āŗā˛¤āŗ ➍⺈➤ā˛ŋ➕ ā˛ĩāŗā˛¯ā˛ĩā˛šā˛žā˛° ā˛…ā˛­āŗā˛¯ā˛žā˛¸ā˛—ā˛ŗāŗ ā˛Ąāŗ†ā˛ĩ➞ā˛Ēā˛°āŗâ€Œā˛—ā˛ŗā˛ŋ➗⺆ ā˛¸āŗā˛¸āŗā˛Ĩā˛ŋ➰ ➆ā˛Ļā˛žā˛¯ā˛Ļ ā˛Žāŗ‚ā˛˛ā˛ĩā˛žā˛—āŗā˛ĩ⺁ā˛Ļ⺁ ā˛Žā˛¤āŗā˛¤āŗ ā˛ļ⺋➎➪⺆➝ ā˛•āŗā˛˛āŗŒā˛Ąāŗ ➏⺇ā˛ĩ⺆➗➺ā˛ŋ➗⺆ ➍ā˛ŋ➜ā˛ĩā˛žā˛Ļ ā˛Ēā˛°āŗā˛¯ā˛žā˛¯ā˛—ā˛ŗāŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ā˛—āŗŒā˛Ēāŗā˛¯ā˛¤āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛—āŗŒā˛°ā˛ĩā˛ŋ➏⺁ā˛ĩ ā˛Ē➰ā˛ŋ➏➰ ā˛ĩāŗā˛¯ā˛ĩā˛¸āŗā˛Ĩāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➰➚ā˛ŋ➏⺁ā˛ĩ⺁ā˛Ļ⺁ ā˛¨ā˛Žāŗā˛Ž ā˛§āŗā˛¯āŗ‡ā˛¯ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†.", "purchase_panel_info_2": "ā˛¨ā˛žā˛ĩ⺁ ā˛Ēāŗ‡ā˛ĩā˛žā˛˛āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏ā˛Ļā˛ŋ➰➞⺁ ā˛Ŧā˛Ļāŗā˛§ā˛°ā˛žā˛—ā˛ŋ➰⺁ā˛ĩ⺁ā˛Ļ➰ā˛ŋ➂ā˛Ļ, ➈ ➖➰⺀ā˛Ļā˛ŋ➝⺁ ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ ➍ā˛ŋā˛Žā˛—āŗ† ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛šāŗ†ā˛šāŗā˛šāŗā˛ĩ➰ā˛ŋ ā˛ĩ⺈ā˛ļā˛ŋā˛ˇāŗā˛Ÿāŗā˛¯ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¨āŗ€ā˛Ąāŗā˛ĩ⺁ā˛Ļā˛ŋā˛˛āŗā˛˛. ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗâ€Œā˛¨ ā˛¨ā˛Ąāŗ†ā˛¯āŗā˛¤āŗā˛¤ā˛ŋ➰⺁ā˛ĩ ➅➭ā˛ŋā˛ĩ⺃ā˛Ļāŗā˛§ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛Ŧ⺆➂ā˛Ŧ➞ā˛ŋ➏➞⺁ ā˛¨ā˛žā˛ĩ⺁ ➍ā˛ŋā˛Žāŗā˛Žā˛‚ā˛¤ā˛š ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛¨āŗā˛¨āŗ ➅ā˛ĩ➞➂ā˛Ŧā˛ŋ➏ā˛ŋā˛Ļāŗā˛Ļāŗ‡ā˛ĩāŗ†.", + "purchase_remove_product_key_prompt": "➍⺀ā˛ĩ⺁ ā˛‰ā˛¤āŗā˛Ēā˛¨āŗā˛¨ ➕⺀➞ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛āŗ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", "purchase_remove_server_product_key": "ā˛¸ā˛°āŗā˛ĩā˛°āŗ ā˛‰ā˛¤āŗā˛Ēā˛¨āŗā˛¨ ➕⺀➞ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛ŋ", "purchase_remove_server_product_key_prompt": "➍⺀ā˛ĩ⺁ ā˛¸ā˛°āŗā˛ĩā˛°āŗ ā˛‰ā˛¤āŗā˛Ēā˛¨āŗā˛¨ ➕⺀➞ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛āŗ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", "purchase_server_description_1": "ā˛‡ā˛Ąāŗ€ ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛—āŗ†", + "purchase_server_title": "ā˛¸ā˛°āŗā˛ĩā˛°āŗ", + "purchase_settings_server_activated": "ā˛¸ā˛°āŗā˛ĩā˛°āŗ ā˛‰ā˛¤āŗā˛Ēā˛¨āŗā˛¨ ➕⺀➞ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛žā˛šā˛•ā˛°āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋā˛¸āŗā˛¤āŗā˛¤ā˛žā˛°āŗ†", "rating_description": "ā˛Žā˛žā˛šā˛ŋ➤ā˛ŋ ā˛Ģ➞➕ā˛Ļā˛˛āŗā˛˛ā˛ŋ EXIF ā˛°āŗ‡ā˛Ÿā˛ŋā˛‚ā˛—āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛Ļā˛°āŗā˛ļā˛ŋ➏ā˛ŋ", + "reassign": "ā˛Žā˛°āŗā˛šā˛‚ā˛šāŗā˛ĩā˛ŋ➕⺆", "reassigned_assets_to_existing_person": "{count, plural, one {# ā˛†ā˛¸āŗā˛¤ā˛ŋ} other {# ā˛†ā˛¸āŗā˛¤ā˛ŋ➗➺⺁}} ā˛…ā˛¨āŗā˛¨āŗ {name, select, null {➒➂ā˛Ļ⺁ ā˛…ā˛¸āŗā˛¤ā˛ŋā˛¤āŗā˛ĩā˛Ļā˛˛āŗā˛˛ā˛ŋ➰⺁ā˛ĩ ā˛ĩāŗā˛¯ā˛•āŗā˛¤ā˛ŋ} other {{name}}} ➗⺆ ā˛Žā˛°āŗ ➍ā˛ŋā˛¯āŗ‹ā˛œā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", "reassing_hint": "ā˛†ā˛¯āŗā˛Ļ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛…ā˛¸āŗā˛¤ā˛ŋā˛¤āŗā˛ĩā˛Ļā˛˛āŗā˛˛ā˛ŋ➰⺁ā˛ĩ ā˛ĩāŗā˛¯ā˛•āŗā˛¤ā˛ŋ➗⺆ ➍ā˛ŋā˛¯āŗ‹ā˛œā˛ŋ➏ā˛ŋ", + "refresh": "➰ā˛ŋā˛Ģāŗā˛°āŗ†ā˛ļāŗ", + "refreshed": "➰ā˛ŋā˛Ģāŗā˛°āŗ†ā˛ļāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", "refreshes_every_file": "ā˛…ā˛¸āŗā˛¤ā˛ŋā˛¤āŗā˛ĩā˛Ļā˛˛āŗā˛˛ā˛ŋ➰⺁ā˛ĩ ā˛Žā˛¤āŗā˛¤āŗ ā˛šāŗŠā˛¸ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛Ģāŗˆā˛˛āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ē⺁➍➃ ➓ā˛Ļāŗā˛¤āŗā˛¤ā˛Ļāŗ†", + "remove": "➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛ŋ", "remove_assets_album_confirmation": "➍⺀ā˛ĩ⺁ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ {count, plural, one {# asset} other {# assets}} ā˛…ā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛āŗ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", "remove_assets_shared_link_confirmation": "➈ ā˛šā˛‚ā˛šā˛ŋ➕⺆➝ ➞ā˛ŋā˛‚ā˛•āŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ {count, plural, one {# asset} other {# assets}} ā˛…ā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛āŗ ➍⺀ā˛ĩ⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", "remove_custom_date_range": "ā˛•ā˛¸āŗā˛Ÿā˛Žāŗ ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛• ā˛ļāŗā˛°āŗ‡ā˛Ŗā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛ŋ", @@ -1029,20 +1258,35 @@ "remove_photo_from_memory": "➈ ➍⺆➍ā˛Ēā˛ŋ➍ā˛ŋ➂ā˛Ļ ā˛Ģāŗ‹ā˛Ÿāŗ‹ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛ŋ", "removed_api_key": "➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛ā˛žā˛Ļ API ➕⺀: {name}", "removed_photo_from_memory": "➍⺆➍ā˛Ēā˛ŋ➍ā˛ŋ➂ā˛Ļ ā˛Ģāŗ‹ā˛Ÿāŗ‹ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "rename": "ā˛Žā˛°āŗā˛šāŗ†ā˛¸ā˛°ā˛ŋ➏ā˛ŋ", + "repair": "ā˛Ļāŗā˛°ā˛¸āŗā˛¤ā˛ŋ", "repair_no_results_message": "ā˛Ÿāŗā˛°āŗā˛¯ā˛žā˛•āŗ ā˛Žā˛žā˛Ąā˛Ļ ā˛Žā˛¤āŗā˛¤āŗ ā˛•ā˛žā˛Ŗāŗ†ā˛¯ā˛žā˛Ļ ā˛Ģāŗˆā˛˛āŗâ€Œā˛—ā˛ŗāŗ ā˛‡ā˛˛āŗā˛˛ā˛ŋ ā˛•ā˛žā˛Ŗā˛ŋ➏ā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗāŗā˛¤āŗā˛¤ā˛ĩāŗ†", + "repository": "➰⺆ā˛Ē⺊➏ā˛ŋ➟➰ā˛ŋ", "require_user_to_change_password_on_first_login": "ā˛ŽāŗŠā˛Ļ➞ ā˛˛ā˛žā˛—ā˛ŋā˛¨āŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°āŗ ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏ā˛Ŧāŗ‡ā˛•ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†", + "reset": "ā˛Žā˛°āŗā˛šāŗŠā˛‚ā˛Ļā˛ŋ➏ā˛ŋ", "reset_pin_code_description": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ēā˛ŋā˛¨āŗ ā˛•āŗ‹ā˛Ąāŗ ā˛…ā˛¨āŗā˛¨āŗ ➍⺀ā˛ĩ⺁ ā˛Žā˛°āŗ†ā˛¤ā˛ŋā˛Ļāŗā˛Ļ➰⺆, ➅ā˛Ļā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗā˛šāŗŠā˛‚ā˛Ļā˛ŋ➏➞⺁ ➍⺀ā˛ĩ⺁ ā˛¸ā˛°āŗā˛ĩā˛°āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛žā˛šā˛•ā˛°ā˛¨āŗā˛¨āŗ ➏➂ā˛Ēā˛°āŗā˛•ā˛ŋ➏ā˛Ŧā˛šāŗā˛Ļ⺁", + "reset_pin_code_with_password": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗâ€Œā˛¨āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ➍⺀ā˛ĩ⺁ ā˛¯ā˛žā˛ĩā˛žā˛—ā˛˛āŗ‚ ➍ā˛ŋā˛Žāŗā˛Ž ā˛Ēā˛ŋā˛¨āŗ ā˛•āŗ‹ā˛Ąāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗā˛šāŗŠā˛‚ā˛Ļā˛ŋ➏ā˛Ŧā˛šāŗā˛Ļ⺁", "reset_sqlite_confirmation": "➍⺀ā˛ĩ⺁ ➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ā˛Ąāŗ‡ā˛Ÿā˛žā˛ĩā˛¨āŗā˛¨āŗ ➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž? ➇ā˛Ļ⺁ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•āŗā˛¤āŗā˛¤ā˛Ļāŗ† ā˛Žā˛¤āŗā˛¤āŗ ➍ā˛ŋā˛Žāŗā˛Žā˛¨āŗā˛¨āŗ ā˛¸āŗˆā˛¨āŗ ā˛”ā˛Ÿāŗ ā˛Žā˛žā˛Ąāŗā˛¤āŗā˛¤ā˛Ļāŗ†.", "reset_sqlite_confirmation_note": "ā˛—ā˛Žā˛¨ā˛ŋ➏ā˛ŋ: ➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋā˛Ļ ➍➂➤➰ ➍⺀ā˛ĩ⺁ ➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗā˛Ēāŗā˛°ā˛žā˛°ā˛‚ā˛­ā˛ŋ➏ā˛Ŧāŗ‡ā˛•ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "reset_sqlite_done": "➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ā˛Ąāŗ‡ā˛Ÿā˛žā˛ĩā˛¨āŗā˛¨āŗ ➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†. ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗā˛Ēāŗā˛°ā˛žā˛°ā˛‚ā˛­ā˛ŋ➏ā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ ā˛Žā˛¤āŗā˛¤āŗ† ā˛˛ā˛žā˛—ā˛ŋā˛¨āŗ ā˛Žā˛žā˛Ąā˛ŋ.", "reset_sqlite_success": "SQLite ā˛Ąāŗ‡ā˛Ÿā˛žā˛Ŧāŗ‡ā˛¸āŗ ā˛…ā˛¨āŗā˛¨āŗ ➝ā˛ļā˛¸āŗā˛ĩā˛ŋā˛¯ā˛žā˛—ā˛ŋ ā˛Žā˛°āŗā˛šāŗŠā˛‚ā˛Ļā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "restore": "ā˛Žā˛°āŗā˛¸āŗā˛Ĩā˛žā˛Ēā˛ŋ➏ā˛ŋ", + "resume": "ā˛Ēāŗā˛¨ā˛°ā˛žā˛°ā˛‚ā˛­", + "role": "ā˛Ēā˛žā˛¤āŗā˛°", "scaffold_body_error_unrecoverable": "➏➰ā˛ŋā˛Ēā˛Ąā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛Ļ ā˛Ļ⺋➎ ➏➂➭ā˛ĩā˛ŋ➏ā˛ŋā˛Ļāŗ†. ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ā˛Ļ⺋➎ā˛ĩā˛¨āŗā˛¨āŗ ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ ā˛Ąā˛ŋā˛¸āŗā˛•ā˛žā˛°āŗā˛Ąāŗ ➅ā˛Ĩā˛ĩā˛ž ➗ā˛ŋā˛Ÿāŗâ€Œā˛šā˛Ŧāŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ ā˛Ÿāŗā˛°āŗ‡ā˛¸āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛¸āŗā˛Ÿāŗā˛¯ā˛žā˛•āŗ ā˛Žā˛žā˛Ąā˛ŋ ➇ā˛Ļ➰ā˛ŋ➂ā˛Ļ ā˛¨ā˛žā˛ĩ⺁ ā˛¸ā˛šā˛žā˛¯ ā˛Žā˛žā˛Ąā˛Ŧā˛šāŗā˛Ļ⺁. ā˛¸ā˛˛ā˛šāŗ† ā˛¨āŗ€ā˛Ąā˛ŋā˛Ļ➰⺆, ➍⺀ā˛ĩ⺁ ➕⺆➺➗ā˛ŋ➍ ➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ā˛Ąāŗ‡ā˛Ÿā˛žā˛ĩā˛¨āŗā˛¨āŗ ➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋ➏ā˛Ŧā˛šāŗā˛Ļ⺁.", "search_by_description_example": "ā˛¸ā˛žā˛Ēā˛žā˛Ļā˛˛āŗā˛˛ā˛ŋ ā˛Ēā˛žā˛Ļā˛¯ā˛žā˛¤āŗā˛°āŗ†ā˛¯ ā˛Ļā˛ŋ➍", "search_by_filename": "ā˛Ģāŗˆā˛˛āŗ ā˛šāŗ†ā˛¸ā˛°āŗ ➅ā˛Ĩā˛ĩā˛ž ā˛ĩā˛ŋā˛¸āŗā˛¤ā˛°ā˛Ŗāŗ†ā˛¯ ā˛Žāŗ‚ā˛˛ā˛• ā˛šāŗā˛Ąāŗā˛•ā˛ŋ", "search_by_filename_example": "➅➂ā˛Ļ➰⺆ IMG_1234.JPG ➅ā˛Ĩā˛ĩā˛ž PNG", + "search_filter_date_title": "ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛• ā˛ļāŗā˛°āŗ‡ā˛Ŗā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋ", + "search_filter_filename": "ā˛Ģāŗˆā˛˛āŗ ā˛šāŗ†ā˛¸ā˛°ā˛ŋ➍ ā˛Žāŗ‚ā˛˛ā˛• ā˛šāŗā˛Ąāŗā˛•ā˛ŋ", "search_for_existing_person": "ā˛…ā˛¸āŗā˛¤ā˛ŋā˛¤āŗā˛ĩā˛Ļā˛˛āŗā˛˛ā˛ŋ➰⺁ā˛ĩ ā˛ĩāŗā˛¯ā˛•āŗā˛¤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛šāŗā˛Ąāŗā˛•ā˛ŋ", "search_no_people_named": "\"{name}\" ā˛šāŗ†ā˛¸ā˛°ā˛ŋ➍ ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ➜➍➰ā˛ŋā˛˛āŗā˛˛", + "search_no_result": "ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛Ģ➞ā˛ŋā˛¤ā˛žā˛‚ā˛ļ➗➺⺁ ā˛•ā˛‚ā˛Ąāŗā˛Ŧ➂ā˛Ļā˛ŋā˛˛āŗā˛˛, ā˛Ŧ⺇➰⺆ ā˛šāŗā˛Ąāŗā˛•ā˛žā˛Ÿ ā˛Ēā˛Ļ ➅ā˛Ĩā˛ĩā˛ž ā˛¸ā˛‚ā˛¯āŗ‹ā˛œā˛¨āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛¯ā˛¤āŗā˛¨ā˛ŋ➏ā˛ŋ", + "search_page_no_objects": "ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛ĩā˛¸āŗā˛¤āŗā˛—ā˛ŗ ā˛Žā˛žā˛šā˛ŋ➤ā˛ŋ ā˛˛ā˛­āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛", + "search_page_no_places": "ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛¸āŗā˛Ĩ➺➗➺ ā˛Žā˛žā˛šā˛ŋ➤ā˛ŋ ā˛˛ā˛­āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛", "search_page_search_photos_videos": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛šāŗā˛Ąāŗā˛•ā˛ŋ", "select_person_to_tag": "ā˛Ÿāŗā˛¯ā˛žā˛—āŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛ĩāŗā˛¯ā˛•āŗā˛¤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋ", + "select_user_for_sharing_page_err_album": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ➰➚ā˛ŋ➏➞⺁ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", "server_restarting_description": "➈ ā˛Ē⺁➟ā˛ĩ⺁ ā˛•āŗā˛ˇā˛Ŗā˛Žā˛žā˛¤āŗā˛°ā˛Ļā˛˛āŗā˛˛ā˛ŋ ➰ā˛ŋā˛Ģāŗā˛°āŗ†ā˛ļāŗ ā˛†ā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", "set_as_album_cover": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ➕ā˛ĩā˛°āŗ ➆➗ā˛ŋ ā˛šāŗŠā˛‚ā˛Ļā˛ŋ➏ā˛ŋ", "set_as_featured_photo": "ā˛ĩ⺈ā˛ļā˛ŋā˛ˇāŗā˛Ÿāŗā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋā˛Ļ ā˛Ģāŗ‹ā˛Ÿāŗ‹ ā˛Žā˛‚ā˛Ļ⺁ ā˛šāŗŠā˛‚ā˛Ļā˛ŋ➏ā˛ŋ", @@ -1053,66 +1297,278 @@ "setting_image_viewer_help": "ā˛ĩā˛ŋā˛ĩ➰ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛•ā˛ĩ⺁ ā˛ŽāŗŠā˛Ļ➞⺁ ā˛¸ā˛Ŗāŗā˛Ŗ ā˛Ĩ➂ā˛Ŧāŗâ€Œā˛¨āŗ‡ā˛˛āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąāŗā˛¤āŗā˛¤ā˛Ļāŗ†, ➍➂➤➰ ā˛Žā˛§āŗā˛¯ā˛Ž ā˛—ā˛žā˛¤āŗā˛°ā˛Ļ ā˛Ēāŗ‚ā˛°āŗā˛ĩā˛ĩāŗ€ā˛•āŗā˛ˇā˛Ŗāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąāŗā˛¤āŗā˛¤ā˛Ļāŗ† (ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋā˛Ļāŗā˛Ļ➰⺆), ➅➂➤ā˛ŋā˛Žā˛ĩā˛žā˛—ā˛ŋ ā˛Žāŗ‚ā˛˛ā˛ĩā˛¨āŗā˛¨āŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąāŗā˛¤āŗā˛¤ā˛Ļāŗ† (ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋā˛Ļāŗā˛Ļ➰⺆).", "setting_image_viewer_original_subtitle": "ā˛Žāŗ‚ā˛˛ ā˛Ēāŗ‚ā˛°āŗā˛Ŗ-ā˛°āŗ†ā˛¸ā˛˛āŗā˛¯āŗ‚ā˛ļā˛¨āŗ ➚ā˛ŋā˛¤āŗā˛°ā˛ĩā˛¨āŗā˛¨āŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ (ā˛ĻāŗŠā˛Ąāŗā˛Ąā˛Ļ⺁!). ā˛Ąāŗ‡ā˛Ÿā˛ž ā˛Ŧā˛ŗā˛•āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛•ā˛Ąā˛ŋā˛Žāŗ† ā˛Žā˛žā˛Ąā˛˛āŗ ➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ (ā˛¨āŗ†ā˛Ÿāŗâ€Œā˛ĩā˛°āŗā˛•āŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛¸ā˛žā˛§ā˛¨ā˛Ļ ā˛¸ā˛‚ā˛—āŗā˛°ā˛š ā˛Žā˛°ā˛Ąā˛°ā˛˛āŗā˛˛āŗ‚).", "setting_image_viewer_preview_subtitle": "ā˛Žā˛§āŗā˛¯ā˛Ž ā˛°āŗ†ā˛¸ā˛˛āŗā˛¯āŗ‚ā˛ļā˛¨āŗ ➚ā˛ŋā˛¤āŗā˛°ā˛ĩā˛¨āŗā˛¨āŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ. ā˛Žāŗ‚ā˛˛ā˛ĩā˛¨āŗā˛¨āŗ ➍⺇➰ā˛ĩā˛žā˛—ā˛ŋ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ➅ā˛Ĩā˛ĩā˛ž ā˛Ĩ➂ā˛Ŧāŗâ€Œā˛¨āŗ‡ā˛˛āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Žā˛žā˛¤āŗā˛° ā˛Ŧ➺➏➞⺁ ➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ.", + "setting_languages_subtitle": "➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗâ€Œā˛¨ ā˛­ā˛žā˛ˇāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏ā˛ŋ", "setting_notifications_notify_failures_grace_period": "ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛ĩ⺈ā˛Ģā˛˛āŗā˛¯ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¸āŗ‚ā˛šā˛ŋ➏ā˛ŋ: {duration}", "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_auto_play_subtitle": "ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗāŗ ➤⺆➰⺆ā˛Ļā˛žā˛— ā˛¸āŗā˛ĩā˛¯ā˛‚ā˛šā˛žā˛˛ā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ēāŗā˛˛āŗ‡ ➆➗➞⺁ ā˛Ēāŗā˛°ā˛žā˛°ā˛‚ā˛­ā˛ŋ➏ā˛ŋ", "setting_video_viewer_original_video_subtitle": "ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ ā˛ĩāŗ€ā˛Ąā˛ŋ➝⺊ā˛ĩā˛¨āŗā˛¨āŗ ā˛¸āŗā˛Ÿāŗā˛°āŗ€ā˛Žāŗ ā˛Žā˛žā˛Ąāŗā˛ĩā˛žā˛—, ā˛Ÿāŗā˛°ā˛žā˛¨āŗā˛¸āŗâ€Œā˛•āŗ‹ā˛Ąāŗ ā˛˛ā˛­āŗā˛¯ā˛ĩā˛ŋā˛Ļāŗā˛Ļ➰⺂ ā˛¸ā˛š ā˛Žāŗ‚ā˛˛ā˛ĩā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛˛āŗ‡ ā˛Žā˛žā˛Ąā˛ŋ. ā˛Ŧā˛Ģ➰ā˛ŋā˛‚ā˛—āŗâ€Œā˛—āŗ† ā˛•ā˛žā˛°ā˛Ŗā˛ĩā˛žā˛—ā˛Ŧā˛šāŗā˛Ļ⺁. ➈ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛˛āŗ†ā˛•āŗā˛•ā˛ŋ➏ā˛Ļāŗ† ā˛¸āŗā˛Ĩ➺⺀➝ā˛ĩā˛žā˛—ā˛ŋ ā˛˛ā˛­āŗā˛¯ā˛ĩā˛ŋ➰⺁ā˛ĩ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Žāŗ‚ā˛˛ ā˛—āŗā˛Ŗā˛Žā˛Ÿāŗā˛Ÿā˛Ļā˛˛āŗā˛˛ā˛ŋ ā˛Ēāŗā˛˛āŗ‡ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", "settings_require_restart": "➈ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛…ā˛¨āŗā˛ĩ➝ā˛ŋ➏➞⺁ ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗā˛Ēāŗā˛°ā˛žā˛°ā˛‚ā˛­ā˛ŋ➏ā˛ŋ", "shared_album_activity_remove_content": "➍⺀ā˛ĩ⺁ ➈ ➚➟⺁ā˛ĩ➟ā˛ŋā˛•āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➅➺ā˛ŋ➏➞⺁ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", + "shared_album_section_people_action_error": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ā˛¤āŗŠā˛°āŗ†ā˛¯āŗā˛ĩā˛žā˛—/➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•āŗā˛ĩā˛žā˛— ā˛Ļ⺋➎ ā˛•ā˛‚ā˛Ąāŗā˛Ŧ➂ā˛Ļā˛ŋā˛Ļāŗ†", + "shared_album_section_people_action_leave": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛ŋ", + "shared_album_section_people_action_remove_user": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛ŋ", + "shared_intent_upload_button_progress_text": "{current} / {total} ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", "shared_link_create_error": "ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛‚ā˛Ą ➞ā˛ŋā˛‚ā˛•āŗ ➰➚ā˛ŋ➏⺁ā˛ĩā˛žā˛— ā˛Ļ⺋➎ ā˛•ā˛‚ā˛Ąāŗā˛Ŧ➂ā˛Ļā˛ŋā˛Ļāŗ†", "shared_link_custom_url_description": "ā˛•ā˛¸āŗā˛Ÿā˛Žāŗ URL ā˛¨āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ➈ ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛‚ā˛Ą ➞ā˛ŋā˛‚ā˛•āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛ĩāŗ‡ā˛ļā˛ŋ➏ā˛ŋ", + "shared_link_edit_description_hint": "ā˛šā˛‚ā˛šā˛ŋ➕⺆ ā˛ĩā˛ŋā˛ĩā˛°ā˛Ŗāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ", + "shared_link_edit_password_hint": "ā˛šā˛‚ā˛šā˛ŋ➕⺆ ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ", "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_second": "{count} ā˛¸āŗ†ā˛•āŗ†ā˛‚ā˛Ąāŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ ➅ā˛ĩ➧ā˛ŋ ā˛Žāŗā˛—ā˛ŋā˛¯āŗā˛¤āŗā˛¤ā˛Ļāŗ†", + "shared_link_expires_seconds": "{count} ā˛¸āŗ†ā˛•āŗ†ā˛‚ā˛Ąāŗā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ ➅ā˛ĩ➧ā˛ŋ ā˛Žāŗā˛—ā˛ŋā˛¯āŗā˛¤āŗā˛¤ā˛Ļāŗ†", "shared_link_password_description": "➈ ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛‚ā˛Ą ➞ā˛ŋā˛‚ā˛•āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛ĩāŗ‡ā˛ļā˛ŋ➏➞⺁ ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛…ā˛—ā˛¤āŗā˛¯ā˛ĩā˛ŋā˛Ļāŗ†", "shared_links_description": "➞ā˛ŋā˛‚ā˛•āŗ ā˛Žāŗ‚ā˛˛ā˛• ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛ŋ", "sharing_enter_password": "➈ ā˛Ē⺁➟ā˛ĩā˛¨āŗā˛¨āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏➞⺁ ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ.", "sharing_page_description": "➍ā˛ŋā˛Žāŗā˛Ž ā˛¨āŗ†ā˛Ÿāŗâ€Œā˛ĩā˛°āŗā˛•āŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ➰⺁ā˛ĩ ā˛œā˛¨ā˛°āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛˛āŗ ā˛šā˛‚ā˛šā˛ŋā˛Ļ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➰➚ā˛ŋ➏ā˛ŋ.", "sharing_sidebar_description": "ā˛¸āŗˆā˛Ąāŗâ€Œā˛Ŧā˛žā˛°āŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ ā˛šā˛‚ā˛šā˛ŋ➕⺆➗⺆ ➞ā˛ŋā˛‚ā˛•āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛Ļā˛°āŗā˛ļā˛ŋ➏ā˛ŋ", "shift_to_permanent_delete": "ā˛†ā˛¸āŗā˛¤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛ļā˛žā˛ļāŗā˛ĩ➤ā˛ĩā˛žā˛—ā˛ŋ ➅➺ā˛ŋ➏➞⺁ ⇧ ā˛’ā˛¤āŗā˛¤ā˛ŋ➰ā˛ŋ", + "show_and_hide_people": "ā˛œā˛¨ā˛°ā˛¨āŗā˛¨āŗ ➤⺋➰ā˛ŋ➏ā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ ā˛Žā˛°āŗ†ā˛Žā˛žā˛Ąā˛ŋ", + "show_in_timeline_setting_description": "➈ ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛° ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛Žāŗā˛Ž ā˛Ÿāŗˆā˛Žāŗâ€Œā˛˛āŗˆā˛¨āŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ ➤⺋➰ā˛ŋ➏ā˛ŋ", + "show_or_hide_info": "ā˛Žā˛žā˛šā˛ŋ➤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➤⺋➰ā˛ŋ➏ā˛ŋ ➅ā˛Ĩā˛ĩā˛ž ā˛Žā˛°āŗ†ā˛Žā˛žā˛Ąā˛ŋ", + "show_supporter_badge_description": "ā˛Ŧ⺆➂ā˛Ŧ➞ā˛ŋ➗➰ ā˛Ŧāŗā˛¯ā˛žā˛Ąāŗā˛œāŗ ➤⺋➰ā˛ŋ➏ā˛ŋ", + "sidebar_display_description": "ā˛¸āŗˆā˛Ąāŗâ€Œā˛Ŧā˛žā˛°āŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛Ŗāŗ†ā˛—āŗ† ➞ā˛ŋā˛‚ā˛•āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛Ļā˛°āŗā˛ļā˛ŋ➏ā˛ŋ", "slideshow_repeat_description": "ā˛¸āŗā˛˛āŗˆā˛Ąāŗâ€Œā˛ļāŗ‹ ā˛•āŗŠā˛¨āŗ†ā˛—āŗŠā˛‚ā˛Ąā˛žā˛— ā˛†ā˛°ā˛‚ā˛­ā˛•āŗā˛•āŗ† ā˛šā˛ŋ➂➤ā˛ŋ➰⺁➗ā˛ŋ", + "sort_created": "ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛• ➰➚ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "sort_items": "ā˛ĩā˛¸āŗā˛¤āŗā˛—ā˛ŗ ā˛¸ā˛‚ā˛–āŗā˛¯āŗ†", + "sort_modified": "ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛• ā˛Žā˛žā˛°āŗā˛Ēā˛Ąā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "sort_newest": "ā˛šāŗŠā˛¸ ā˛Ģāŗ‹ā˛Ÿāŗ‹", + "sort_oldest": "ā˛šā˛ŗāŗ†ā˛¯ ā˛Ģāŗ‹ā˛Ÿāŗ‹", + "sort_people_by_similarity": "ā˛šāŗ‹ā˛˛ā˛ŋ➕⺆➝ ā˛†ā˛§ā˛žā˛°ā˛Ļ ā˛Žāŗ‡ā˛˛āŗ† ā˛œā˛¨ā˛°ā˛¨āŗā˛¨āŗ ā˛ĩā˛ŋā˛‚ā˛—ā˛Ąā˛ŋ➏ā˛ŋ", + "sort_recent": "ā˛¤āŗ€ā˛°ā˛ž ā˛‡ā˛¤āŗā˛¤āŗ€ā˛šā˛ŋ➍ ā˛Ģāŗ‹ā˛Ÿāŗ‹", + "sort_title": "ā˛ļāŗ€ā˛°āŗā˛ˇā˛ŋ➕⺆", + "stack": "ā˛¸āŗā˛Ÿā˛žā˛•āŗ", + "stack_duplicates": "ā˛¸āŗā˛Ÿā˛žā˛•āŗ ➍➕➞⺁➗➺⺁", "stack_select_one_photo": "ā˛¸āŗā˛Ÿāŗā˛¯ā˛žā˛•āŗâ€Œā˛—ā˛žā˛—ā˛ŋ ➒➂ā˛Ļ⺁ ā˛Žāŗā˛–āŗā˛¯ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛ĩā˛¨āŗā˛¨āŗ ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋ", + "stack_selected_photos": "ā˛†ā˛¯āŗā˛Ļ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛œāŗ‹ā˛Ąā˛ŋ➏ā˛ŋ", + "stacktrace": "ā˛¸āŗā˛Ÿā˛žā˛•āŗā˛Ÿāŗā˛°āŗ‡ā˛¸āŗ", + "start": "ā˛Ēāŗā˛°ā˛žā˛°ā˛‚ā˛­", + "start_date": "ā˛Ēāŗā˛°ā˛žā˛°ā˛‚ā˛­ ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛•", "start_date_before_end_date": "➆➰➂➭ā˛Ļ ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛•ā˛ĩ⺁ ➅➂➤ā˛ŋā˛Ž ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛•ā˛•āŗā˛•ā˛ŋ➂➤ ā˛ŽāŗŠā˛Ļ➞⺁ ➇➰ā˛Ŧ⺇➕⺁", + "state": "ā˛°ā˛žā˛œāŗā˛¯", + "status": "ā˛¸āŗā˛Ĩā˛ŋ➤ā˛ŋ", + "stop_casting": "ā˛Ŧā˛ŋā˛¤āŗā˛¤ā˛°ā˛ŋ➏⺁ā˛ĩā˛ŋā˛•āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛˛āŗā˛˛ā˛ŋ➏ā˛ŋ", + "stop_motion_photo": "ā˛šā˛˛ā˛¨āŗ†ā˛¯ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛ĩā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛˛āŗā˛˛ā˛ŋ➏ā˛ŋ", + "stop_photo_sharing": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗāŗā˛ĩ⺁ā˛Ļā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛˛āŗā˛˛ā˛ŋ➏⺁ā˛ĩ⺁ā˛Ļāŗ‡?", + "stop_photo_sharing_description": "{partner} ā˛‡ā˛¨āŗā˛¨āŗ ā˛Žāŗā˛‚ā˛Ļāŗ† ➍ā˛ŋā˛Žāŗā˛Ž ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛ĩāŗ‡ā˛ļā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛žā˛—āŗā˛ĩ⺁ā˛Ļā˛ŋā˛˛āŗā˛˛.", "stop_sharing_photos_with_user": "➈ ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ➍ā˛ŋā˛Žāŗā˛Ž ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗāŗā˛ĩ⺁ā˛Ļā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛˛āŗā˛˛ā˛ŋ➏ā˛ŋ", + "storage": "ā˛ļāŗ‡ā˛–ā˛°ā˛Ŗā˛ž ā˛¸āŗā˛Ĩ➺", + "storage_label": "ā˛ļāŗ‡ā˛–ā˛°ā˛Ŗā˛ž ➞⺇ā˛Ŧā˛˛āŗ", + "storage_quota": "ā˛ļāŗ‡ā˛–ā˛°ā˛Ŗā˛ž ā˛•āŗ‹ā˛Ÿā˛ž", + "submit": "ā˛¸ā˛˛āŗā˛˛ā˛ŋ➏ā˛ŋ", + "success": "➝ā˛ļā˛¸āŗā˛¸āŗ", + "suggestions": "ā˛¸ā˛˛ā˛šāŗ†ā˛—ā˛ŗāŗ", + "sunrise_on_the_beach": "ā˛•ā˛Ąā˛˛ā˛¤āŗ€ā˛°ā˛Ļā˛˛āŗā˛˛ā˛ŋ ā˛¸āŗ‚ā˛°āŗā˛¯āŗ‹ā˛Ļ➝", + "support": "ā˛Ŧ⺆➂ā˛Ŧ➞", + "support_and_feedback": "ā˛Ŧ⺆➂ā˛Ŧ➞ ā˛Žā˛¤āŗā˛¤āŗ ā˛Ēāŗā˛°ā˛¤ā˛ŋā˛•āŗā˛°ā˛ŋ➝⺆", "support_third_party_description": "➍ā˛ŋā˛Žāŗā˛Ž ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ā˛¸āŗā˛Ĩā˛žā˛Ēā˛¨āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛Žāŗ‚ā˛°ā˛¨āŗ‡ ā˛ĩāŗā˛¯ā˛•āŗā˛¤ā˛ŋ➝ā˛ŋ➂ā˛Ļ ā˛Ēāŗā˛¯ā˛žā˛•āŗ‡ā˛œāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†. ➍⺀ā˛ĩ⺁ ➅➍⺁➭ā˛ĩā˛ŋ➏⺁ā˛ĩ ā˛¸ā˛Žā˛¸āŗā˛¯āŗ†ā˛—ā˛ŗāŗ ➆ ā˛Ēāŗā˛¯ā˛žā˛•āŗ‡ā˛œāŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ ā˛‰ā˛‚ā˛Ÿā˛žā˛—ā˛ŋ➰ā˛Ŧā˛šāŗā˛Ļ⺁, ➆ā˛Ļāŗā˛Ļ➰ā˛ŋ➂ā˛Ļ ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ➕⺆➺➗ā˛ŋ➍ ➞ā˛ŋā˛‚ā˛•āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏ā˛ŋā˛•āŗŠā˛‚ā˛Ąāŗ ā˛ŽāŗŠā˛Ļ➞ ➏➂ā˛Ļā˛°āŗā˛­ā˛Ļā˛˛āŗā˛˛ā˛ŋ ➅ā˛ĩā˛°āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ā˛¸ā˛Žā˛¸āŗā˛¯āŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Žā˛¤āŗā˛¤ā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛ŋ.", + "supporter": "ā˛Ŧ⺆➂ā˛Ŧ➞ā˛ŋ➗", + "swap_merge_direction": "ā˛¸āŗā˛ĩā˛žā˛Ēāŗ ā˛ĩā˛ŋ➞⺀➍ ➍ā˛ŋā˛°āŗā˛Ļāŗ‡ā˛ļ➍", + "sync": "➏ā˛ŋā˛‚ā˛•āŗ", + "sync_albums": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➏ā˛ŋā˛‚ā˛•āŗ ā˛Žā˛žā˛Ąā˛ŋ", "sync_albums_manual_subtitle": "➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛ŋā˛Ļ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋā˛Ļ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—ā˛ŗā˛ŋ➗⺆ ➏ā˛ŋā˛‚ā˛•āŗ ā˛Žā˛žā˛Ąā˛ŋ", + "sync_local": "ā˛¸āŗā˛Ĩ➺⺀➝ ➏ā˛ŋā˛‚ā˛•āŗ ā˛Žā˛žā˛Ąā˛ŋ", + "sync_remote": "➏ā˛ŋā˛‚ā˛•āŗ ➰ā˛ŋā˛Žāŗ‹ā˛Ÿāŗ", + "sync_status": "➏ā˛ŋā˛‚ā˛•āŗ ā˛¸āŗā˛Ĩā˛ŋ➤ā˛ŋ", "sync_status_subtitle": "➏ā˛ŋā˛‚ā˛•āŗ ā˛ĩāŗā˛¯ā˛ĩā˛¸āŗā˛Ĩāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", "sync_upload_album_setting_subtitle": "ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ ā˛†ā˛¯āŗā˛Ļ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—ā˛ŗā˛ŋ➗⺆ ➍ā˛ŋā˛Žāŗā˛Ž ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➰➚ā˛ŋ➏ā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛ŋ", + "tag": "ā˛Ÿāŗā˛¯ā˛žā˛—āŗ ā˛Žā˛žā˛Ąā˛ŋ", + "tag_assets": "ā˛Ÿāŗā˛¯ā˛žā˛—āŗ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗāŗ", + "tag_feature_description": "ā˛¤ā˛žā˛°āŗā˛•ā˛ŋ➕ ā˛Ÿāŗā˛¯ā˛žā˛—āŗ ā˛ĩā˛ŋ➎➝➗➺ ā˛Žāŗ‚ā˛˛ā˛• ➗⺁➂ā˛Ē⺁ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛Ļ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ŧāŗā˛°āŗŒā˛¸āŗ ā˛Žā˛žā˛Ąāŗā˛ĩ⺁ā˛Ļ⺁", "tag_not_found_question": "ā˛Ÿāŗā˛¯ā˛žā˛—āŗ ➏ā˛ŋā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛˛āŗā˛˛ā˛ĩāŗ‡? Create a new tag.", + "tag_people": "ā˛Ÿāŗā˛¯ā˛žā˛—āŗ ➜➍➰⺁", + "tags": "ā˛Ÿāŗā˛¯ā˛žā˛—āŗā˛—ā˛ŗāŗ", + "tap_to_run_job": "➕⺆➞➏ā˛ĩā˛¨āŗā˛¨āŗ ā˛šā˛˛ā˛žā˛¯ā˛ŋ➏➞⺁ ā˛Ÿāŗā˛¯ā˛žā˛Ēāŗ ā˛Žā˛žā˛Ąā˛ŋ", + "template": "ā˛Ÿāŗ†ā˛‚ā˛Ēāŗā˛˛āŗ‡ā˛Ÿāŗ", + "text_recognition": "ā˛Ēā˛ āŗā˛¯ ➗⺁➰⺁➤ā˛ŋ➏⺁ā˛ĩā˛ŋ➕⺆", + "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": "ā˛Žāŗ‚ā˛°āŗ ā˛šā˛‚ā˛¤ā˛Ļ ā˛˛āŗ‹ā˛Ąā˛ŋā˛‚ā˛—āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "then": "➍➂➤➰", "they_will_be_merged_together": "➅ā˛ĩāŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛’ā˛Ÿāŗā˛Ÿā˛ŋ➗⺆ ā˛ĩā˛ŋā˛˛āŗ€ā˛¨ā˛—āŗŠā˛ŗā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†", + "third_party_resources": "ā˛Žāŗ‚ā˛°ā˛¨āŗ‡ ā˛­ā˛žā˛—ā˛Ļ ➏➂ā˛Ēā˛¨āŗā˛Žāŗ‚ā˛˛ā˛—ā˛ŗāŗ", + "time": "ā˛¸ā˛Žā˛¯", + "time_based_memories": "ā˛¸ā˛Žā˛¯ ā˛†ā˛§ā˛žā˛°ā˛ŋ➤ ➍⺆➍ā˛Ē⺁➗➺⺁", "time_based_memories_duration": "ā˛Ēāŗā˛°ā˛¤ā˛ŋ ➚ā˛ŋā˛¤āŗā˛°ā˛ĩā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛Ļā˛°āŗā˛ļā˛ŋ➏➞⺁ ā˛¸āŗ†ā˛•āŗ†ā˛‚ā˛Ąāŗā˛—ā˛ŗ ā˛¸ā˛‚ā˛–āŗā˛¯āŗ†.", + "timeline": "ā˛Ÿāŗˆā˛Žāŗ ā˛˛āŗˆā˛¨āŗ", + "timezone": "ā˛¸ā˛Žā˛¯ā˛ĩ➞➝", + "to_archive": "ā˛†ā˛°āŗā˛•āŗˆā˛ĩāŗ", + "to_change_password": "ā˛Ēā˛žā˛¸āŗā˛ĩā˛°āŗā˛Ąāŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏ā˛ŋ", + "to_favorite": "ā˛¨āŗ†ā˛šāŗā˛šā˛ŋ➍", + "to_login": "ā˛˛ā˛žā˛—ā˛ŋā˛¨āŗ", + "to_multi_select": "ā˛Ŧā˛šāŗ ā˛†ā˛¯āŗā˛•āŗ†ā˛—āŗ†", + "to_parent": "ā˛Ē⺋➎➕➰ ā˛Ŧ➺ā˛ŋ➗⺆ ā˛šāŗ‹ā˛—ā˛ŋ", + "to_select": "ā˛†ā˛¯āŗā˛•āŗ† ā˛Žā˛žā˛Ąā˛˛āŗ", + "to_trash": "➅➍⺁ā˛Ēā˛¯āŗā˛•āŗā˛¤", + "toggle_settings": "ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗ ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ÿā˛žā˛—ā˛˛āŗ ā˛Žā˛žā˛Ąā˛ŋ", + "toggle_theme_description": "ā˛Ĩāŗ€ā˛Žāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ÿā˛žā˛—ā˛˛āŗ ā˛Žā˛žā˛Ąā˛ŋ", + "total": "ā˛’ā˛Ÿāŗā˛Ÿāŗ", + "total_usage": "ā˛’ā˛Ÿāŗā˛Ÿāŗ ā˛Ŧ➺➕⺆", + "trash": "➅➍⺁ā˛Ēā˛¯āŗā˛•āŗā˛¤", + "trash_all": "ā˛Žā˛˛āŗā˛˛ā˛ž ➅➍⺁ā˛Ēā˛¯āŗā˛•āŗā˛¤", + "trash_delete_asset": "➅➍⺁ā˛Ēā˛¯āŗā˛•āŗā˛¤ / ➅➺ā˛ŋ➏⺁ ā˛†ā˛¸āŗā˛¤ā˛ŋ", + "trash_emptied": "ā˛–ā˛žā˛˛ā˛ŋ ➕➏", "trash_no_results_message": "➅➍⺁ā˛Ēā˛¯āŗā˛•āŗā˛¤ā˛•āŗā˛•āŗ† ā˛ĩā˛°āŗā˛—ā˛žā˛¯ā˛ŋā˛¸ā˛˛ā˛žā˛Ļ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗāŗ ā˛‡ā˛˛āŗā˛˛ā˛ŋ ā˛•ā˛žā˛Ŗā˛ŋ➏ā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗāŗā˛¤āŗā˛¤ā˛ĩāŗ†.", + "trash_page_delete_all": "ā˛Žā˛˛āŗā˛˛ā˛ĩā˛¨āŗā˛¨āŗ‚ ➅➺ā˛ŋ➏ā˛ŋ", "trash_page_empty_trash_dialog_content": "➍ā˛ŋā˛Žāŗā˛Ž ➅➍⺁ā˛Ēā˛¯āŗā˛•āŗā˛¤ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛–ā˛žā˛˛ā˛ŋ ā˛Žā˛žā˛Ąā˛˛āŗ ➍⺀ā˛ĩ⺁ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž? ➈ ā˛ā˛Ÿā˛‚ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ ā˛ļā˛žā˛ļāŗā˛ĩ➤ā˛ĩā˛žā˛—ā˛ŋ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†", "trash_page_info": "➅➍⺁ā˛Ēā˛¯āŗā˛•āŗā˛¤ā˛•āŗā˛•āŗ† ➏⺇➰ā˛ŋ➏ā˛ŋā˛Ļ ā˛ā˛Ÿā˛‚ā˛—ā˛ŗā˛¨āŗā˛¨āŗ {days} ā˛Ļā˛ŋ➍➗➺ ➍➂➤➰ ā˛ļā˛žā˛ļāŗā˛ĩ➤ā˛ĩā˛žā˛—ā˛ŋ ➅➺ā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†", + "trash_page_no_assets": "➕➏ā˛Ļ ā˛†ā˛¸āŗā˛¤ā˛ŋ ā˛‡ā˛˛āŗā˛˛", + "trash_page_restore_all": "ā˛Žā˛˛āŗā˛˛ā˛ĩā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗā˛¸āŗā˛Ĩā˛žā˛Ēā˛ŋ➏ā˛ŋ", + "trash_page_select_assets_btn": "ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋ", + "trigger": "ā˛Ÿāŗā˛°ā˛ŋā˛—āŗā˛—ā˛°āŗ", + "trigger_asset_uploaded": "ā˛†ā˛¸āŗā˛¤ā˛ŋ ➅ā˛Ēāŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", "trigger_asset_uploaded_description": "ā˛šāŗŠā˛¸ ā˛¸āŗā˛ĩā˛¤āŗā˛¤ā˛¨āŗā˛¨āŗ ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛ŋā˛Ļā˛žā˛— ā˛Ÿāŗā˛°ā˛ŋā˛—ā˛°āŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†", "trigger_description": "➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛žā˛°ā˛‚ā˛­ā˛ŋ➏⺁ā˛ĩ ➒➂ā˛Ļ⺁ ā˛˜ā˛Ÿā˛¨āŗ†", + "trigger_person_recognized": "ā˛ĩāŗā˛¯ā˛•āŗā˛¤ā˛ŋ ➗⺁➰⺁➤ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", "trigger_person_recognized_description": "➒ā˛Ŧāŗā˛Ŧ ā˛ĩāŗā˛¯ā˛•āŗā˛¤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛Ēā˛¤āŗā˛¤āŗ†ā˛šā˛šāŗā˛šā˛ŋā˛Ļā˛žā˛— ā˛Ēāŗā˛°ā˛šāŗ‹ā˛Ļā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†", + "trigger_type": "ā˛Ÿāŗā˛°ā˛ŋā˛—āŗā˛—ā˛°āŗ ā˛Ēāŗā˛°ā˛•ā˛žā˛°", + "troubleshoot": "ā˛¤āŗŠā˛‚ā˛Ļ➰⺆", + "type": "➟⺈ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛ŋ", "unable_to_change_pin_code": "ā˛Ēā˛ŋā˛¨āŗ ā˛•āŗ‹ā˛Ąāŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛˛āŗā˛˛", "unable_to_check_version": "➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ➅ā˛Ĩā˛ĩā˛ž ā˛¸ā˛°āŗā˛ĩā˛°āŗ ➆ā˛ĩāŗƒā˛¤āŗā˛¤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛Ē➰ā˛ŋā˛ļ⺀➞ā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛˛āŗā˛˛", "unable_to_setup_pin_code": "ā˛Ēā˛ŋā˛¨āŗ ā˛•āŗ‹ā˛Ąāŗ ā˛¸āŗ†ā˛Ÿā˛Ēāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛˛āŗā˛˛", + "unarchive": "ā˛…ā˛°ā˛žā˛œā˛•ā˛¤ā˛žā˛ĩā˛žā˛Ļā˛ŋ", + "unfavorite": "ā˛…ā˛šā˛ŋ➤➕➰", + "unhide_person": "ā˛¸ā˛šā˛žā˛¯ā˛• ā˛ĩāŗā˛¯ā˛•āŗā˛¤ā˛ŋ", + "unknown": "ā˛…ā˛œāŗā˛žā˛žā˛¤", + "unknown_country": "ā˛…ā˛œāŗā˛žā˛žā˛¤ ā˛Ļāŗ‡ā˛ļ", + "unknown_date": "ā˛…ā˛œāŗā˛žā˛žā˛¤ ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛•", + "unknown_year": "ā˛…ā˛œāŗā˛žā˛žā˛¤ ā˛ĩā˛°āŗā˛ˇ", + "unlimited": "➅➍ā˛ŋā˛¯ā˛Žā˛ŋ➤", + "unlink_motion_video": "ā˛šā˛˛ā˛¨āŗ†ā˛¯ ā˛ĩāŗ€ā˛Ąā˛ŋ➝⺊ā˛ĩā˛¨āŗā˛¨āŗ ā˛…ā˛¨āŗā˛˛ā˛ŋā˛‚ā˛•āŗ ā˛Žā˛žā˛Ąā˛ŋ", + "unmute_memories": "ā˛…ā˛¨āŗā˛šā˛ŋ➤ ➍⺆➍ā˛Ē⺁➗➺⺁", + "unnamed_album": "ā˛šāŗ†ā˛¸ā˛°ā˛ŋ➏ā˛Ļ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ", + "unnamed_album_delete_confirmation": "➍⺀ā˛ĩ⺁ ➈ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ā˛…ā˛¨āŗā˛¨āŗ ➅➺ā˛ŋ➏➞⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", + "unnamed_share": "ā˛šāŗ†ā˛¸ā˛°ā˛ŋ➏ā˛Ļ ā˛Ēā˛žā˛˛āŗ", + "unsaved_change": "➉➺ā˛ŋ➏ā˛Ļ ā˛Ŧā˛Ļā˛˛ā˛žā˛ĩ➪⺆", + "unselect_all": "ā˛Žā˛˛āŗā˛˛ā˛ĩā˛¨āŗā˛¨āŗ ā˛†ā˛¯āŗā˛•āŗ† ā˛Žā˛žā˛Ąā˛ŋ", + "unselect_all_duplicates": "ā˛Žā˛˛āŗā˛˛ā˛ž ā˛¨ā˛•ā˛˛āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛†ā˛¯āŗā˛•āŗ† ā˛Žā˛žā˛Ąā˛Ŧāŗ‡ā˛Ąā˛ŋ", + "unstack": "ā˛…ā˛¨āŗ-ā˛¸āŗā˛Ÿā˛žā˛•āŗ", + "unsupported_field_type": "ā˛Ŧ⺆➂ā˛Ŧ➞ā˛ŋ➏ā˛Ļ ā˛•āŗā˛ˇāŗ‡ā˛¤āŗā˛° ā˛Ēāŗā˛°ā˛•ā˛žā˛°", "unsupported_file_type": "{file} ā˛Ģāŗˆā˛˛āŗ ā˛Ēāŗā˛°ā˛•ā˛žā˛°ā˛ĩ⺁ ā˛Ŧ⺆➂ā˛Ŧ➞ā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋā˛˛āŗā˛˛ā˛Ļ {type} ā˛•ā˛žā˛°ā˛Ŗ ➅ā˛Ļā˛¨āŗā˛¨āŗ ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛.", + "untagged": "ā˛…ā˛¨āŗā˛Ÿā˛žā˛—āŗā˛Ąāŗ", + "untitled_workflow": "ā˛ļāŗ€ā˛°āŗā˛ˇā˛ŋā˛•āŗ†ā˛°ā˛šā˛ŋ➤ ➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩ⺁", + "up_next": "ā˛Žāŗā˛‚ā˛Ļā˛ŋ➍ ➅ā˛Ēāŗ", "update_location_action_prompt": "{count} ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋā˛Ļ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗ ā˛¸āŗā˛Ĩ➺ā˛ĩā˛¨āŗā˛¨āŗ ➇ā˛Ļā˛°āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ➍ā˛ĩ⺀➕➰ā˛ŋ➏ā˛ŋ:", + "updated_at": "➍ā˛ĩ⺀➕➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "updated_password": "ā˛Ēā˛žā˛¸āŗā˛ĩā˛°āŗā˛Ąāŗ ➍ā˛ĩ⺀➕➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "upload": "➅ā˛Ēāŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛ŋ", + "upload_concurrency": "➅ā˛Ēāŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛•ā˛¨āŗā˛•āŗā˛¯āŗā˛°āŗ†ā˛¨āŗā˛¸ā˛ŋ", + "upload_details": "➅ā˛Ēāŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛ĩā˛ŋā˛ĩ➰➗➺⺁", "upload_dialog_info": "ā˛†ā˛¯āŗā˛•āŗ†ā˛Žā˛žā˛Ąā˛ŋā˛Ļ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗ(ā˛—ā˛ŗā˛¨āŗā˛¨āŗ) ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛—āŗ† ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ➍⺀ā˛ĩ⺁ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", + "upload_dialog_title": "➅ā˛Ēāŗā˛˛āŗ‹ā˛Ąāŗ ā˛†ā˛¸āŗā˛¤ā˛ŋ", "upload_errors": "{count, plural, one {# ā˛Ļ⺋➎} other {# ā˛Ļ⺋➎➗➺⺁}} ā˛¨āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Ēāŗ‚ā˛°āŗā˛Ŗā˛—āŗŠā˛‚ā˛Ąā˛ŋā˛Ļāŗ†, ā˛šāŗŠā˛¸ ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¨āŗ‹ā˛Ąā˛˛āŗ ā˛Ē⺁➟ā˛ĩā˛¨āŗā˛¨āŗ ➰ā˛ŋā˛Ģāŗā˛°āŗ†ā˛ļāŗ ā˛Žā˛žā˛Ąā˛ŋ.", + "upload_finished": "➅ā˛Ēāŗā˛˛āŗ‹ā˛Ąāŗ ā˛Žāŗā˛—ā˛ŋā˛Ļā˛ŋā˛Ļāŗ†", + "upload_status_duplicates": "➍➕➞⺁", + "upload_status_errors": "ā˛Ļ⺋➎➗➺⺁", + "upload_status_uploaded": "➅ā˛Ēāŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "upload_success": "➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ➝ā˛ļā˛¸āŗā˛ĩā˛ŋā˛¯ā˛žā˛—ā˛ŋā˛Ļāŗ†, ā˛šāŗŠā˛¸ ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¨āŗ‹ā˛Ąā˛˛āŗ ā˛Ē⺁➟ā˛ĩā˛¨āŗā˛¨āŗ ➰ā˛ŋā˛Ģāŗā˛°āŗ†ā˛ļāŗ ā˛Žā˛žā˛Ąā˛ŋ.", + "upload_to_immich": "ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ({count}) ➗⺆ ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛ŋ", + "uploading": "➅ā˛Ēāŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛†ā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", + "uploading_media": "ā˛Žā˛žā˛§āŗā˛¯ā˛Žā˛ĩā˛¨āŗā˛¨āŗ ➅ā˛Ēāŗ ā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", + "usage": "ā˛Ŧ➺➕⺆", + "use_biometric": "ā˛Ŧā˛¯āŗ‹ā˛Žāŗ†ā˛Ÿāŗā˛°ā˛ŋā˛•āŗ ā˛Ŧ➺➏ā˛ŋ", + "use_browser_locale": "ā˛Ŧāŗā˛°āŗŒā˛¸ā˛°āŗ ā˛˛āŗŠā˛•āŗ‡ā˛˛āŗ ā˛Ŧ➺➏ā˛ŋ", + "use_browser_locale_description": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ŧāŗā˛°āŗŒā˛¸ā˛°āŗ ā˛¸āŗā˛Ĩ➺ā˛ĩā˛¨āŗā˛¨āŗ ➆➧➰ā˛ŋ➏ā˛ŋ ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛•ā˛—ā˛ŗāŗ, ā˛¸ā˛Žā˛¯ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛¸ā˛‚ā˛–āŗā˛¯āŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ģā˛žā˛°āŗā˛Žāŗā˛¯ā˛žā˛Ÿāŗ ā˛Žā˛žā˛Ąā˛ŋ", + "use_current_connection": "ā˛Ēāŗā˛°ā˛¸āŗā˛¤āŗā˛¤ ➏➂ā˛Ēā˛°āŗā˛•ā˛ĩā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏ā˛ŋ", "use_custom_date_range": "ā˛Ŧā˛Ļ➞ā˛ŋ➗⺆ ā˛•ā˛¸āŗā˛Ÿā˛Žāŗ ā˛Ļā˛ŋā˛¨ā˛žā˛‚ā˛• ā˛ļāŗā˛°āŗ‡ā˛Ŗā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏ā˛ŋ", + "user": "ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°", "user_has_been_deleted": "➈ ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛¨āŗā˛¨āŗ ➅➺ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†.", + "user_id": "ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛° ID", + "user_pin_code_settings": "ā˛Ēā˛ŋā˛¨āŗ ā˛•āŗ‹ā˛Ąāŗ", + "user_pin_code_settings_description": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ēā˛ŋā˛¨āŗ ā˛•āŗ‹ā˛Ąāŗ ā˛…ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", + "user_privacy": "ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛° ā˛—āŗŒā˛Ēāŗā˛¯ā˛¤āŗ†", + "user_purchase_settings": "➖➰⺀ā˛Ļā˛ŋ", + "user_purchase_settings_description": "➍ā˛ŋā˛Žāŗā˛Ž ➖➰⺀ā˛Ļā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", + "user_usage_detail": "ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛° ā˛Ŧ➺➕⺆➝ ā˛ĩā˛ŋā˛ĩ➰", + "user_usage_stats": "ā˛–ā˛žā˛¤āŗ† ā˛Ŧ➺➕⺆➝ ➅➂➕ā˛ŋ➅➂ā˛ļ➗➺⺁", + "user_usage_stats_description": "ā˛–ā˛žā˛¤āŗ† ā˛Ŧ➺➕⺆➝ ➅➂➕ā˛ŋ➅➂ā˛ļā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "username": "ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛šāŗ†ā˛¸ā˛°āŗ", + "users": "ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°āŗ", + "utilities": "➉ā˛Ēā˛¯āŗā˛•āŗā˛¤ā˛¤āŗ†ā˛—ā˛ŗāŗ", + "validate": "ā˛ŽāŗŒā˛˛āŗā˛¯āŗ€ā˛•ā˛°ā˛ŋ➏ā˛ŋ", "validate_endpoint_error": "ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ā˛Žā˛žā˛¨āŗā˛¯ā˛ĩā˛žā˛Ļ URL ā˛…ā˛¨āŗā˛¨āŗ ā˛¨ā˛Žāŗ‚ā˛Ļā˛ŋ➏ā˛ŋ", + "validation_error": "ā˛•āŗā˛°ā˛Žā˛Ŧā˛Ļāŗā˛§ ā˛Ļ⺋➎", + "variables": "ā˛…ā˛¸āŗā˛Ĩā˛ŋ➰➗➺⺁", + "version": "➆ā˛ĩāŗƒā˛¤āŗā˛¤ā˛ŋ", + "version_announcement_closing": "➍ā˛ŋā˛Žāŗā˛Ž ā˛¸āŗā˛¨āŗ‡ā˛šā˛ŋ➤, ā˛…ā˛˛āŗ†ā˛•āŗā˛¸āŗ", "version_announcement_message": "ā˛¨ā˛Žā˛¸āŗā˛•ā˛žā˛°! ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗâ€Œā˛¨ ā˛šāŗŠā˛¸ ➆ā˛ĩāŗƒā˛¤āŗā˛¤ā˛ŋ ā˛˛ā˛­āŗā˛¯ā˛ĩā˛ŋā˛Ļāŗ†. ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ➤ā˛Ēāŗā˛Ē⺁ ā˛¸ā˛‚ā˛°ā˛šā˛¨āŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¤ā˛Ąāŗ†ā˛—ā˛Ÿāŗā˛Ÿā˛˛āŗ ➍ā˛ŋā˛Žāŗā˛Ž ā˛¸āŗ†ā˛Ÿā˛Ēāŗ ➍ā˛ĩāŗ€ā˛•āŗƒā˛¤ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ† ā˛Žā˛‚ā˛Ļ⺁ ā˛–ā˛šā˛ŋ➤ā˛Ēā˛Ąā˛ŋ➏ā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛˛āŗ, ā˛ĩā˛ŋā˛ļ⺇➎ā˛ĩā˛žā˛—ā˛ŋ ➍⺀ā˛ĩ⺁ ā˛ĩā˛žā˛šāŗâ€Œā˛Ÿā˛ĩā˛°āŗ ➅ā˛Ĩā˛ĩā˛ž ➍ā˛ŋā˛Žāŗā˛Ž ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ➍ā˛ŋā˛Ļā˛°āŗā˛ļ➍ā˛ĩā˛¨āŗā˛¨āŗ ā˛¸āŗā˛ĩā˛¯ā˛‚ā˛šā˛žā˛˛ā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ➍ā˛ĩ⺀➕➰ā˛ŋ➏⺁ā˛ĩ⺁ā˛Ļā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏⺁ā˛ĩ ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛•ā˛žā˛°āŗā˛¯ā˛ĩā˛ŋā˛§ā˛žā˛¨ā˛ĩā˛¨āŗā˛¨āŗ ā˛Ŧā˛ŗā˛¸āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗā˛Ļ➰⺆, ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ release notes ➓ā˛Ļ➞⺁ ā˛¸āŗā˛ĩā˛˛āŗā˛Ē ā˛¸ā˛Žā˛¯ ➤⺆➗⺆ā˛Ļāŗā˛•āŗŠā˛ŗāŗā˛ŗā˛ŋ.", + "version_history": "➆ā˛ĩāŗƒā˛¤āŗā˛¤ā˛ŋ ➇➤ā˛ŋā˛šā˛žā˛¸", + "version_history_item": "{date} ➰➂ā˛Ļ⺁ {version} ā˛…ā˛¨āŗā˛¨āŗ ā˛¸āŗā˛Ĩā˛žā˛Ēā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "video": "ā˛ĩāŗ€ā˛Ąā˛ŋ➝⺊", "video_hover_setting": "ā˛šāŗ‹ā˛ĩā˛°āŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ ā˛ĩāŗ€ā˛Ąā˛ŋ➝⺊ ā˛Ĩ➂ā˛Ŧāŗâ€Œā˛¨āŗ‡ā˛˛āŗ ā˛Ēāŗā˛˛āŗ‡ ā˛Žā˛žā˛Ąā˛ŋ", "video_hover_setting_description": "ā˛ŽāŗŒā˛¸āŗ ā˛ā˛Ÿā˛‚ ā˛Žāŗ‡ā˛˛āŗ† ➏⺁➺ā˛ŋā˛Ļā˛žā˛Ąāŗā˛¤āŗā˛¤ā˛ŋ➰⺁ā˛ĩā˛žā˛— ā˛ĩāŗ€ā˛Ąā˛ŋ➝⺊ ā˛Ĩ➂ā˛Ŧāŗâ€Œā˛¨āŗ‡ā˛˛āŗ ā˛Ēāŗā˛˛āŗ‡ ā˛Žā˛žā˛Ąā˛ŋ. ➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋā˛Ļāŗā˛Ļ➰⺂ ā˛¸ā˛š, ā˛Ēāŗā˛˛āŗ‡ ā˛ā˛•ā˛žā˛¨āŗ ā˛Žāŗ‡ā˛˛āŗ† ➏⺁➺ā˛ŋā˛Ļā˛žā˛Ąāŗā˛ĩ ā˛Žāŗ‚ā˛˛ā˛• ā˛Ēāŗā˛˛āŗ‡ā˛Ŧāŗā˛¯ā˛žā˛•āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛žā˛°ā˛‚ā˛­ā˛ŋ➏ā˛Ŧā˛šāŗā˛Ļ⺁.", + "videos": "ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗāŗ", + "videos_only": "ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗāŗ ā˛Žā˛žā˛¤āŗā˛°", + "view": "ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "view_album": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "view_all": "ā˛Žā˛˛āŗā˛˛ā˛ĩā˛¨āŗā˛¨āŗ‚ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "view_all_users": "ā˛Žā˛˛āŗā˛˛ā˛ž ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛¨āŗā˛¨āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "view_asset_owners": "ā˛†ā˛¸āŗā˛¤ā˛ŋ ā˛Žā˛žā˛˛āŗ€ā˛•ā˛°ā˛¨āŗā˛¨āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "view_details": "ā˛ĩā˛ŋā˛ĩā˛°ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "view_in_timeline": "ā˛Ÿāŗˆā˛Žāŗ ā˛˛āŗˆā˛¨āŗ ā˛¨ā˛˛āŗā˛˛ā˛ŋ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "view_link": "➞ā˛ŋā˛‚ā˛•āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "view_links": "➞ā˛ŋā˛‚ā˛•āŗ ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "view_name": "ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "view_next_asset": "ā˛Žāŗā˛‚ā˛Ļā˛ŋ➍ ā˛†ā˛¸āŗā˛¤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "view_previous_asset": "ā˛šā˛ŋ➂ā˛Ļā˛ŋ➍ ā˛†ā˛¸āŗā˛¤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "view_qr_code": "ā˛•āŗā˛¯āŗ‚ā˛†ā˛°āŗ ā˛•āŗ‹ā˛Ąāŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "view_similar_photos": "➇ā˛Ļāŗ‡ ➰⺀➤ā˛ŋ➝ ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "view_stack": "ā˛¸āŗā˛Ÿā˛žā˛•āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "view_user": "ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛¨āŗā˛¨āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "viewer_remove_from_stack": "ā˛¸āŗā˛Ÿā˛žā˛•āŗā˛¨ā˛ŋ➂ā˛Ļ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛ŋ", + "viewer_stack_use_as_main_asset": "ā˛Žāŗā˛–āŗā˛¯ ā˛†ā˛¸āŗā˛¤ā˛ŋā˛¯ā˛žā˛—ā˛ŋ ā˛Ŧ➺➏ā˛ŋ", + "viewer_unstack": "ā˛…ā˛¨āŗ-ā˛¸āŗā˛Ÿā˛žā˛•āŗ", + "visibility": "ā˛—āŗ‹ā˛šā˛°ā˛¤āŗ†", + "visual": "ā˛ĩā˛ŋā˛ˇāŗā˛¯ā˛˛āŗ", + "visual_builder": "ā˛ĩā˛ŋā˛ˇāŗā˛¯ā˛˛āŗ ā˛Ŧā˛ŋā˛˛āŗā˛Ąā˛°āŗ", + "waiting": "ā˛•ā˛žā˛¯ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†", + "warning": "ā˛Žā˛šāŗā˛šā˛°ā˛ŋ➕⺆", + "week": "ā˛ĩā˛žā˛°", + "welcome": "ā˛¸āŗā˛ĩā˛žā˛—ā˛¤", + "welcome_to_immich": "ā˛¸āŗā˛ĩā˛žā˛—ā˛¤ ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ", + "width": "➅➗➞", + "wifi_name": "ā˛ĩ⺈-ā˛Ģ⺈ ā˛šāŗ†ā˛¸ā˛°āŗ", "workflow_delete_prompt": "➈ ā˛ĩā˛°āŗā˛•āŗâ€Œā˛Ģāŗā˛˛āŗ‹ ā˛…ā˛¨āŗā˛¨āŗ ➅➺ā˛ŋ➏➞⺁ ➍⺀ā˛ĩ⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", + "workflow_deleted": "➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩ⺁ ➅➺ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "workflow_description": "➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩā˛ŋ➍ ā˛ĩā˛ŋā˛ĩ➰➪⺆", + "workflow_info": "➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩā˛ŋ➍ ā˛Žā˛žā˛šā˛ŋ➤ā˛ŋ", + "workflow_json": "➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩ⺁ JSON", "workflow_json_help": "JSON ā˛¸āŗā˛ĩ➰⺂ā˛Ēā˛Ļā˛˛āŗā˛˛ā˛ŋ ➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩā˛ŋ➍ ā˛¸ā˛‚ā˛°ā˛šā˛¨āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➏➂ā˛Ēā˛žā˛Ļā˛ŋ➏ā˛ŋ. ā˛Ŧā˛Ļā˛˛ā˛žā˛ĩ➪⺆➗➺⺁ ā˛Ļ⺃ā˛ļāŗā˛¯ ā˛Ŧā˛ŋā˛˛āŗā˛Ąā˛°āŗâ€Œā˛—āŗ† ➏ā˛ŋā˛‚ā˛•āŗ ā˛†ā˛—āŗā˛¤āŗā˛¤ā˛ĩāŗ†.", + "workflow_name": "➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩā˛ŋ➍ ā˛šāŗ†ā˛¸ā˛°āŗ", "workflow_navigation_prompt": "➍ā˛ŋā˛Žāŗā˛Ž ā˛Ŧā˛Ļā˛˛ā˛žā˛ĩā˛Ŗāŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➉➺ā˛ŋ➏ā˛Ļ⺆➝⺇ ➍⺀ā˛ĩ⺁ ā˛šāŗŠā˛°ā˛Ąā˛˛āŗ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", + "workflow_summary": "➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩā˛ŋ➍ ā˛¸ā˛žā˛°ā˛žā˛‚ā˛ļ", + "workflow_update_success": "➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩā˛¨āŗā˛¨āŗ ➝ā˛ļā˛¸āŗā˛ĩā˛ŋā˛¯ā˛žā˛—ā˛ŋ ➍ā˛ĩ⺀➕➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "workflow_updated": "➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩā˛¨āŗā˛¨āŗ ➍ā˛ĩ⺀➕➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "workflows": "➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩ⺁➗➺⺁", "workflows_help_text": "ā˛Ÿāŗā˛°ā˛ŋā˛—āŗā˛—ā˛°āŗâ€Œā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛Ģā˛ŋā˛˛āŗā˛Ÿā˛°āŗâ€Œā˛—ā˛ŗ ā˛†ā˛§ā˛žā˛°ā˛Ļ ā˛Žāŗ‡ā˛˛āŗ† ➍ā˛ŋā˛Žāŗā˛Ž ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗ ā˛Žāŗ‡ā˛˛ā˛ŋ➍ ā˛•āŗā˛°ā˛ŋā˛¯āŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩ⺁➗➺⺁ ā˛¸āŗā˛ĩā˛¯ā˛‚ā˛šā˛žā˛˛ā˛ŋā˛¤ā˛—āŗŠā˛ŗā˛ŋā˛¸āŗā˛¤āŗā˛¤ā˛ĩāŗ†", + "wrong_pin_code": "➤ā˛Ēāŗā˛Ēā˛žā˛Ļ ā˛Ēā˛ŋā˛¨āŗ ā˛•āŗ‹ā˛Ąāŗ", + "year": "ā˛ĩā˛°āŗā˛ˇ", + "yes": "ā˛šāŗŒā˛Ļ⺁", "you_dont_have_any_shared_links": "➍⺀ā˛ĩ⺁ ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛šā˛‚ā˛šā˛ŋā˛•āŗŠā˛‚ā˛Ą ➞ā˛ŋā˛‚ā˛•āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛šāŗŠā˛‚ā˛Ļā˛ŋā˛˛āŗā˛˛", - "zero_to_clear_rating": "ā˛†ā˛¸āŗā˛¤ā˛ŋ ā˛°āŗ‡ā˛Ÿā˛ŋā˛‚ā˛—āŗ ā˛…ā˛¨āŗā˛¨āŗ ➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ 0 ā˛’ā˛¤āŗā˛¤ā˛ŋ➰ā˛ŋ" + "your_wifi_name": "➍ā˛ŋā˛Žāŗā˛Ž ā˛ĩ⺈-ā˛Ģ⺈ ā˛šāŗ†ā˛¸ā˛°āŗ", + "zero_to_clear_rating": "ā˛†ā˛¸āŗā˛¤ā˛ŋ ā˛°āŗ‡ā˛Ÿā˛ŋā˛‚ā˛—āŗ ā˛…ā˛¨āŗā˛¨āŗ ➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ 0 ā˛’ā˛¤āŗā˛¤ā˛ŋ➰ā˛ŋ", + "zoom_image": "ā˛œāŗ‚ā˛Žāŗ ā˛‡ā˛Žāŗ‡ā˛œāŗ", + "zoom_to_bounds": "ā˛Žā˛Ąā˛ŋ➕➞⺁" } diff --git a/i18n/ko.json b/i18n/ko.json index 22d5d1b8a6..fff2c3d10a 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -441,7 +441,7 @@ "user_successfully_removed": "ė‚ŦėšŠėž {email}ë‹˜ė´ ė„ąęŗĩ렁ėœŧ로 ė‚­ė œë˜ė—ˆėŠĩ니다.", "users_page_description": "관ëĻŦėž ė‚ŦėšŠėž íŽ˜ė´ė§€", "version_check_enabled_description": "ë˛„ė „ í™•ė¸ í™œė„ąí™”", - "version_check_implications": "ėŖŧ揰렁ėœŧ로 Github뗐 ėš”ė˛­ė„ ëŗ´ë‚´ 냈 ë˛„ė „ė„ í™•ė¸í•Šë‹ˆë‹¤.", + "version_check_implications": "ėŖŧ揰렁ėœŧ로 {server}뗐 ėš”ė˛­ė„ ëŗ´ë‚´ 냈 ë˛„ė „ė„ í™•ė¸í•Šë‹ˆë‹¤.", "version_check_settings": "ë˛„ė „ í™•ė¸", "version_check_settings_description": "냈 ë˛„ė „ í™•ė¸ 및 ė•ŒëĻŧ 기ëŠĨė„ 관ëĻŦ합니다.", "video_conversion_job": "ë™ė˜ėƒ íŠ¸ëžœėŠ¤ėŊ”드", @@ -798,7 +798,7 @@ "command_palette_to_close": "ë‹Ģ기", "command_palette_to_navigate": "ë“¤ė–´ę°€ę¸°", "command_palette_to_select": "ė„ íƒí•˜ę¸°", - "command_palette_to_show_all": "다 ëŗ´ė—ŦėŖŧ기", + "command_palette_to_show_all": "ëĒ¨ë‘ ëŗ´ę¸°", "comment_deleted": "ëŒ“ę¸€ė´ ė‚­ė œë˜ė—ˆėŠĩ니다.", "comment_options": "댓글 ė˜ĩė…˜", "comments_and_likes": "댓글 및 ėĸ‹ė•„ėš”", @@ -849,9 +849,12 @@ "create_link_to_share": "ęŗĩ뜠 링íŦ ėƒė„ą", "create_link_to_share_description": "링íŦ가 ėžˆëŠ” ę˛Ŋ뚰 누ęĩŦ나 ė„ íƒí•œ ė‚Ŧė§„ė„ ëŗŧ 눘 ėžˆėŠĩ니다.", "create_new": "ėƒˆëĄœ 만들기", + "create_new_face": "냈 ė–ŧęĩ´ ėƒė„ą", "create_new_person": "ė¸ëŦŧ ėƒė„ą", "create_new_person_hint": "ė„ íƒí•œ 항ëĒŠė˜ ė¸ëŦŧė„ 냈 ė¸ëŦŧ로 ëŗ€ę˛Ŋ", "create_new_user": "냈 ė‚ŦėšŠėž ėƒė„ą", + "create_person": "ė¸ëŦŧ ėƒė„ą", + "create_person_subtitle": "ė„ íƒí•œ ė–ŧęĩ´ė— ė´ëĻ„ė„ ėļ”가해 ė‹ ęˇœ ė¸ëŦŧė„ ėƒė„ąí•˜ęŗ  태그 맀렕", "create_shared_album_page_share_add_assets": "항ëĒŠ ėļ”ę°€", "create_shared_album_page_share_select_photos": "ė‚Ŧė§„ ė„ íƒ", "create_shared_link": "ęŗĩ뜠 링íŦ ėƒė„ą", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "ęŗ ė •", "crop_aspect_ratio_free": "링렑 ėĄ°ė ˆ", "crop_aspect_ratio_original": "ė›ëŗ¸", + "crop_aspect_ratio_square": "ė •ė‚Ŧ각형", "curated_object_page_title": "ė‚ŦëŦŧ", "current_device": "현ėžŦ 기기", "current_pin_code": "현ėžŦ PIN ėŊ”드", @@ -880,7 +884,7 @@ "daily_title_text_date": "Mė›” dėŧ EEEE", "daily_title_text_date_year": "yyyy년 Mė›” dėŧ EEEE", "dark": "다íŦ", - "dark_theme": "다íŦ 테마 토글", + "dark_theme": "다íŦ 테마 ė „í™˜", "date": "ë‚ ė§œ", "date_after": "ë‹¤ėŒ ë‚ ė§œ ė´í›„", "date_and_time": "ë‚ ė§œ 및 ė‹œę°„", @@ -891,10 +895,8 @@ "day": "ėŧ", "days": "ėŧ", "deduplicate_all": "ëĒ¨ë‘ ė‚­ė œ", - "deduplication_criteria_1": "ė´ë¯¸ė§€ íŦ기 (ë°”ė´íŠ¸)", - "deduplication_criteria_2": "EXIF ė •ëŗ´ 항ëĒŠ 눘", - "deduplication_info": "ëš„ėŠˇí•œ 항ëĒŠ ė •ëŗ´", - "deduplication_info_description": "항ëĒŠė„ ėžë™ėœŧ로 미ëĻŦ ė„ íƒí•˜ęŗ , ëš„ėŠˇí•œ 항ëĒŠė„ ęĩŦëļ„í•  때 ë‹¤ėŒ ė •ëŗ´ëĨŧ ė°¸ęŗ í•Šë‹ˆë‹¤:", + "default_locale": "ę¸°ëŗ¸ 로ėŧ€ėŧ", + "default_locale_description": "브ëŧėš°ė € 로ėŧ€ėŧ 네렕뗐 따ëŧ ë‚ ė§œ 및 ėˆĢėž í˜•ė‹ė„ ė§€ė •í•Šë‹ˆë‹¤", "delete": "ė‚­ė œ", "delete_action_confirmation_message": "ė´ 항ëĒŠė„ ė‚­ė œí•˜ė‹œę˛ ėŠĩ니까? ė„œë˛„ė—ė„œëŠ” 항ëĒŠė„ íœ´ė§€í†ĩėœŧ로 ė´ë™ė‹œí‚¤ëŠ°, 로ėģŦė—ė„œë„ ė‚­ė œí•  ę˛ƒė¸ė§€ í™•ė¸ ëŠ”ė‹œė§€ę°€ í‘œė‹œëŠë‹ˆë‹¤.", "delete_action_prompt": "{count}氜 항ëĒŠ ė‚­ė œë¨", @@ -1007,6 +1009,8 @@ "editor_edits_applied_success": "íŽ¸ė§‘ė´ ė ėšŠë˜ė—ˆėŠĩ니다.", "editor_flip_horizontal": "ėĸŒėš°ë°˜ė „", "editor_flip_vertical": "ėƒí•˜ë°˜ė „", + "editor_handle_corner": "{corner, select, top_left {ėĸŒėƒë‹¨} top_right {ėš°ėƒë‹¨} bottom_left {ėĸŒí•˜ë‹¨} bottom_right {ėš°í•˜ë‹¨} other {A}} ėŊ”너 핸들", + "editor_handle_edge": "{edge, select, top {ėœ„} bottom {ė•„ëž˜} left {ė™ŧėĒŊ} right {똤ëĨ¸ėĒŊ} other {An}} ëĒ¨ė„œëĻŦ 핸들", "editor_orientation": "ë°Ší–Ĩ", "editor_reset_all_changes": "íŽ¸ė§‘ë‚´ėšŠ ė´ˆę¸°í™”", "editor_rotate_left": "ë°˜ė‹œęŗ„ ë°Ší–Ĩėœŧ로 90° íšŒė „", @@ -1065,26 +1069,26 @@ "failed_to_load_assets": "항ëĒŠ 로드 ė‹¤íŒ¨", "failed_to_load_notifications": "ė•ŒëĻŧ 로드 ė‹¤íŒ¨", "failed_to_load_people": "ė¸ëŦŧ 로드 ė‹¤íŒ¨", - "failed_to_remove_product_key": "ė œí’ˆ 키 ė œęą°ė— ė‹¤íŒ¨", + "failed_to_remove_product_key": "ė œí’ˆ 키 ė œęą°ė— ė‹¤íŒ¨í–ˆėŠĩ니다.", "failed_to_reset_pin_code": "PIN ėŊ”드 ė´ˆę¸°í™” ė‹¤íŒ¨", - "failed_to_stack_assets": "항ëĒŠ ėŠ¤íƒė— ė‹¤íŒ¨", - "failed_to_unstack_assets": "항ëĒŠ ėŠ¤íƒ í’€ę¸°ė— ė‹¤íŒ¨", + "failed_to_stack_assets": "항ëĒŠ ėŠ¤íƒė— ė‹¤íŒ¨í–ˆėŠĩ니다.", + "failed_to_unstack_assets": "항ëĒŠ ėŠ¤íƒ í’€ę¸°ė— ė‹¤íŒ¨í–ˆėŠĩ니다.", "failed_to_update_notification_status": "ė•ŒëĻŧ ėƒíƒœ ė—…ë°ė´íŠ¸ ė‹¤íŒ¨", "incorrect_email_or_password": "ėž˜ëĒģ된 ė´ëŠ”ėŧ 또는 비밀번호", "library_folder_already_exists": "氀렏ė˜Ŧ ę˛Ŋ로가 ė´ë¯¸ ėĄ´ėžŦ합니다.", - "page_not_found": "íŽ˜ė´ė§€ëĨŧ ė°žė„ 눘 ė—†ėŒ :/", + "page_not_found": "íŽ˜ė´ė§€ëĨŧ ė°žė„ 눘 ė—†ėŒ", "paths_validation_failed": "{paths, plural, one {ę˛Ŋ로 #氜} other {ę˛Ŋ로 #氜}}가 ėœ íš¨ė„ą 검ė‚Ŧ뗐 ė‹¤íŒ¨í–ˆėŠĩ니다.", "profile_picture_transparent_pixels": "프로필 ė‚Ŧ맄뗐 íˆŦëĒ… í”Ŋė…€ė„ ė‚ŦėšŠí•  눘 ė—†ėŠĩ니다. ė‚Ŧė§„ė„ 확대하거나 ė´ë™í•˜ė„¸ėš”.", "quota_higher_than_disk_size": "í• ë‹šëŸ‰ė€ ë””ėŠ¤íŦ íŦę¸°ëŗ´ë‹¤ ėž‘ė•„ė•ŧ 합니다.", "something_went_wrong": "ëŦ¸ė œę°€ ë°œėƒí–ˆėŠĩ니다.", - "unable_to_add_album_users": "ė•¨ë˛”ė— ė‚ŦėšŠėžëĨŧ ėļ”가할 눘 ė—†ėŒ", - "unable_to_add_assets_to_shared_link": "항ëĒŠė„ ęŗĩ뜠 링íŦ뗐 ėļ”가할 눘 ė—†ėŒ", - "unable_to_add_comment": "ëŒ“ę¸€ė„ ėļ”가할 눘 ė—†ėŒ", - "unable_to_add_exclusion_pattern": "ė œė™¸ ęˇœėš™ė„ ėļ”가할 눘 ė—†ėŒ", - "unable_to_add_partners": "파트너ëĨŧ ėļ”가할 눘 ė—†ėŒ", - "unable_to_add_remove_archive": "{archived, select, true {ëŗ´ę´€í•¨ė—ė„œ 항ëĒŠė„ ė œęą°í• } other {ëŗ´ę´€í•¨ėœŧ로 항ëĒŠė„ ė´ë™í• }} 눘 ė—†ėŒ", - "unable_to_add_remove_favorites": "ėĻę˛¨ė°žę¸°ė— 항ëĒŠė„ {favorite, select, true {ėļ”ę°€} other {ė œęą°}}할 눘 ė—†ėŒ", - "unable_to_archive_unarchive": "항ëĒŠė„ {archived, select, true {ëŗ´ę´€} other {ëŗ´ę´€ í•´ė œ}}할 눘 ė—†ėŒ", + "unable_to_add_album_users": "ė•¨ë˛”ė— ė‚ŦėšŠėžëĨŧ ėļ”가할 눘 ė—†ėŠĩ니다.", + "unable_to_add_assets_to_shared_link": "항ëĒŠė„ ęŗĩ뜠 링íŦ뗐 ėļ”가할 눘 ė—†ėŠĩ니다.", + "unable_to_add_comment": "ëŒ“ę¸€ė„ ėļ”가할 눘 ė—†ėŠĩ니다.", + "unable_to_add_exclusion_pattern": "ė œė™¸ ęˇœėš™ė„ ėļ”가할 눘 ė—†ėŠĩ니다.", + "unable_to_add_partners": "파트너ëĨŧ ėļ”가할 눘 ė—†ėŠĩ니다.", + "unable_to_add_remove_archive": "{archived, select, true {ëŗ´ę´€í•¨ė—ė„œ 항ëĒŠė„ ė œęą°í• } other {ëŗ´ę´€í•¨ėœŧ로 항ëĒŠė„ ė´ë™í• }} 눘 ė—†ėŠĩ니다.", + "unable_to_add_remove_favorites": "ėĻę˛¨ė°žę¸°ė— 항ëĒŠė„ {favorite, select, true {ėļ”ę°€} other {ė œęą°}}할 눘 ė—†ėŠĩ니다", + "unable_to_archive_unarchive": "항ëĒŠė„ {archived, select, true {ëŗ´ę´€} other {ëŗ´ę´€ í•´ė œ}}할 눘 ė—†ėŠĩ니다", "unable_to_change_album_user_role": "ė•¨ë˛” ė‚ŦėšŠėžė˜ ė—­í• ė„ ëŗ€ę˛Ŋ할 눘 ė—†ėŠĩ니다.", "unable_to_change_date": "ë‚ ė§œëĨŧ ëŗ€ę˛Ŋ할 눘 ė—†ėŠĩ니다.", "unable_to_change_description": "네ëĒ…ė„ ëŗ€ę˛Ŋ할 눘 ė—†ėŠĩ니다.", @@ -1130,10 +1134,10 @@ "unable_to_remove_library": "ëŧė´ë¸ŒëŸŦëĻŦëĨŧ ė œęą°í•  눘 ė—†ėŠĩ니다.", "unable_to_remove_partner": "파트너ëĨŧ ė œęą°í•  눘 ė—†ėŠĩ니다.", "unable_to_remove_reaction": "ë°˜ė‘ė„ ė œęą°í•  눘 ė—†ėŠĩ니다.", - "unable_to_reset_password": "비밀번호ëĨŧ ė´ˆę¸°í™”í•  눘 ė—†ėŒ", + "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": "ė•¨ë˛”ė„ ė €ėžĨ할 눘 ė—†ėŠĩ니다.", @@ -1146,7 +1150,7 @@ "unable_to_scan_library": "ëŧė´ë¸ŒëŸŦëĻŦëĨŧ 늤ėē”í•  눘 ė—†ėŠĩ니다.", "unable_to_set_feature_photo": "대표 ė‚Ŧė§„ė„ ė„¤ė •í•  눘 ė—†ėŠĩ니다.", "unable_to_set_profile_picture": "프로필 ė‚Ŧė§„ė„ ė„¤ė •í•  눘 ė—†ėŠĩ니다.", - "unable_to_set_rating": "í‰ė ė„ ė •í•  눘 ė—†ėŒ", + "unable_to_set_rating": "ëŗ„ė ė„ ė§€ė •í•  눘 ė—†ėŠĩ니다.", "unable_to_submit_job": "ėž‘ė—…ė„ ėˆ˜í–‰í•  눘 ė—†ėŠĩ니다.", "unable_to_trash_asset": "íœ´ė§€í†ĩėœŧ로 ė´ë™í•  눘 ė—†ėŠĩ니다.", "unable_to_unlink_account": "ęŗ„ė • ė—°ę˛°ė„ í•´ė œí•  눘 ė—†ėŠĩ니다.", @@ -1385,9 +1389,11 @@ "library_page_sort_title": "ė•¨ë˛”ëĒ…", "licenses": "ëŧė´ė„ ėŠ¤", "light": "ëŧė´íŠ¸", + "light_theme": "ëŧė´íŠ¸ 테마로 ė „í™˜", "like": "ėĸ‹ė•„ėš”", "like_deleted": "ėĸ‹ė•„ėš”ę°€ ė‚­ė œë˜ė—ˆėŠĩ니다.", "link_motion_video": "ëĒ¨ė…˜ ëš„ë””ė˜¤ 링íŦ", + "link_to_docs": "ėžė„¸í•œ ë‚´ėšŠė€ ëŦ¸ė„œëĨŧ ė°¸ėĄ°í•˜ė‹­ė‹œė˜¤.", "link_to_oauth": "OAuth뗐 뗰枰", "linked_oauth_account": "OAuth ęŗ„ė •ė´ ė—°ę˛°ë˜ė—ˆėŠĩ니다.", "list": "ëĒŠëĄ", @@ -1649,6 +1655,7 @@ "only_favorites": "ėĻę˛¨ė°žę¸°ë§Œ", "open": "뗴揰", "open_calendar": "ėē˜ëĻ°ë” 뗴揰", + "open_in_browser": "브ëŧėš°ė €ė—ė„œ 뗴揰", "open_in_map_view": "ė§€ë„ ëŗ´ę¸°ė—ė„œ 뗴揰", "open_in_openstreetmap": "OpenStreetMapė—ė„œ 뗴揰", "open_the_search_filters": "ę˛€ėƒ‰ 필터 뗴揰", @@ -1805,11 +1812,11 @@ "purchase_settings_server_activated": "ė„œë˛„ ė œí’ˆ 키는 관ëĻŦėžę°€ ė œė–´í•Šë‹ˆë‹¤.", "query_asset_id": "ėŋŧëĻŦ 항ëĒŠ ID", "queue_status": "렄랴 {total}, {count} 대기 뤑", - "rate_asset": "항ëĒŠ í‰ė ", + "rate_asset": "항ëĒŠ ëŗ„ė ", "rating": "ëŗ„ė ", - "rating_clear": "í‰ė  ė´ˆę¸°í™”", - "rating_count": "{count, plural, =0 {í‰ė  ė—†ėŒ} one {#렐} other {#렐}}", - "rating_description": "ėƒė„¸ ė •ëŗ´ íŒ¨ë„ė— EXIF 등급 태그 í‘œė‹œ", + "rating_clear": "ëŗ„ė  ė´ˆę¸°í™”", + "rating_count": "{count, plural, =0 {ëŗ„ė  ė—†ėŒ} one {#렐} other {#렐}}", + "rating_description": "ėƒė„¸ ė •ëŗ´ íŒ¨ë„ė— EXIF ëŗ„ė  태그 í‘œė‹œ", "reaction_options": "ë°˜ė‘ ė˜ĩė…˜", "read_changelog": "ëŗ€ę˛Ŋ ë‚´ė—­ ëŗ´ę¸°", "readonly_mode_disabled": "ėŊ기 ė „ėšŠ ëĒ¨ë“œ ëš„í™œė„ąí™”", @@ -1927,6 +1934,7 @@ "search_by_filename": "파ėŧëĒ… 또는 확ėžĨėžëĄœ ę˛€ėƒ‰", "search_by_filename_example": "똈: IMG_1234.JPG 또는 PNG", "search_by_ocr": "OCR로 ę˛€ėƒ‰", + "search_by_ocr_example": "ëŧë–ŧ", "search_camera_lens_model": "렌ėψ ëĒ¨ë¸ ę˛€ėƒ‰...", "search_camera_make": "ėš´ëŠ”ëŧ ė œėĄ°ė‚Ŧ ę˛€ėƒ‰...", "search_camera_model": "ėš´ëŠ”ëŧ ëĒ¨ë¸ëĒ… ę˛€ėƒ‰...", @@ -1946,7 +1954,7 @@ "search_filter_media_type_title": "ë¯¸ë””ė–´ ėĸ…ëĨ˜ ė„ íƒ", "search_filter_ocr": "OCR ę˛€ėƒ‰", "search_filter_people_title": "ė¸ëŦŧ ė„ íƒ", - "search_filter_star_rating": "í‰ė ", + "search_filter_star_rating": "ëŗ„ė ", "search_filter_tags_title": "태그 ė„ íƒ", "search_for": "ę˛€ėƒ‰", "search_for_existing_person": "ėĄ´ėžŦ하는 ė¸ëŦŧ ę˛€ėƒ‰", @@ -1968,7 +1976,7 @@ "search_page_your_map": "ë‚˜ė˜ ė§€ë„", "search_people": "ė¸ëŦŧ ę˛€ėƒ‰", "search_places": "ėžĨė†Œ ę˛€ėƒ‰", - "search_rating": "등급ėœŧ로 ę˛€ėƒ‰...", + "search_rating": "ëŗ„ė ėœŧ로 ę˛€ėƒ‰...", "search_result_page_new_search_hint": "냈 ę˛€ėƒ‰", "search_settings": "네렕 ę˛€ėƒ‰", "search_state": "맀뗭 ę˛€ėƒ‰...", @@ -1990,6 +1998,7 @@ "select_all_in": "{group}ė˜ ëĒ¨ë“  항ëĒŠ ė„ íƒ", "select_avatar_color": "ė•„ë°”íƒ€ ėƒ‰ėƒ ė„ íƒ", "select_count": "{count, plural, one {# ė„ íƒė¤‘} other {# ė„ íƒė¤‘}}", + "select_cutoff_date": "ėœ ė§€ 기간 네렕", "select_face": "ė–ŧęĩ´ ė„ íƒ", "select_featured_photo": "대표 ė‚Ŧė§„ ė„ íƒ", "select_from_computer": "ėģ´í“¨í„°ė—ė„œ ė„ íƒ", @@ -2388,6 +2397,7 @@ "viewer_remove_from_stack": "ėŠ¤íƒė—ė„œ ė œęą°", "viewer_stack_use_as_main_asset": "대표 항ëĒŠėœŧ로 네렕", "viewer_unstack": "ėŠ¤íƒ 풀기", + "visibility": "í‘œė‹œ 네렕", "visibility_changed": "ė¸ëŦŧ {count, plural, one {#ëĒ…} other {#ëĒ…}}ė˜ í‘œė‹œ ė—Ŧëļ€ę°€ ëŗ€ę˛Ŋ됨", "visual": "비ėŖŧė–ŧ", "visual_builder": "비ėŖŧė–ŧ 빌더", @@ -2418,7 +2428,7 @@ "yes": "네", "you_dont_have_any_shared_links": "ęŗĩ뜠 링íŦ가 ė—†ėŠĩ니다.", "your_wifi_name": "Wi-Fi ë„¤íŠ¸ė›ŒíŦ ė´ëĻ„", - "zero_to_clear_rating": "0ė„ 눌ëŸŦ 항ëĒŠ í‰ė  ė´ˆę¸°í™”", + "zero_to_clear_rating": "0ė„ 눌ëŸŦ 항ëĒŠ ëŗ„ė  ė´ˆę¸°í™”", "zoom_image": "ė´ë¯¸ė§€ 확대", "zoom_to_bounds": "í™”ëŠ´ė— 맞ėļ° í™•ëŒ€" } diff --git a/i18n/lt.json b/i18n/lt.json index 5675673317..b686e2526d 100644 --- a/i18n/lt.json +++ b/i18n/lt.json @@ -441,7 +441,7 @@ "user_successfully_removed": "Naudotojas {email} sėkmingai paÅĄalintas.", "users_page_description": "AdministratoriÅŗ vartotojÅŗ puslapis", "version_check_enabled_description": "ÄŽgalinti versijÅŗ tikrinimą", - "version_check_implications": "VersijÅŗ tikrinimas reikalauja periodiÅĄkos komunikacijos su github.com", + "version_check_implications": "VersijÅŗ tikrinimas reikalauja periodiÅĄkos komunikacijos su {server}", "version_check_settings": "Versijos tikrinimas", "version_check_settings_description": "ÄŽjungti/iÅĄjungti naujos versijos praneÅĄimus", "video_conversion_job": "Vaizdo įraÅĄÅŗ konvertavimas", @@ -849,9 +849,12 @@ "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_face": "Sukurti naują veidą", "create_new_person": "Sukurti naują ÅžmogÅŗ", "create_new_person_hint": "Priskirti pasirinktus elementus naujam Åžmogui", "create_new_user": "Sukurti naują varotoją", + "create_person": "Sukurti asmenį", + "create_person_subtitle": "Pridėkite vardą prie pasirinkto veido, kad sukurtumėte ir paÅžymėtumėte naują asmenį", "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ą", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "UÅžfiksuota", "crop_aspect_ratio_free": "Nefiksuota", "crop_aspect_ratio_original": "Originalus", + "crop_aspect_ratio_square": "Kvadratas", "curated_object_page_title": "Daiktai", "current_device": "Dabartinis įrenginys", "current_pin_code": "Dabartinis PIN kodas", @@ -880,7 +884,7 @@ "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "Tamsi", - "dark_theme": "Perjungti tamsią temą", + "dark_theme": "Perjungti į tamsią temą", "date": "Data", "date_after": "Data po", "date_and_time": "Data ir laikas", @@ -891,10 +895,8 @@ "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": "Numatytoji Vietovė", + "default_locale_description": "Formatuoti datas ir skaičius pagal savo 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", @@ -970,7 +972,7 @@ "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)", + "duplicates_description": "Tvarkyti kiekvieną elementÅŗ grupę nurodant elementus, kurie yra dublikatai (jei tokiÅŗ yra).", "duration": "Trukmė", "edit": "Redaguoti", "edit_album": "Redaguoti albumą", @@ -1213,7 +1215,7 @@ "file_name_text": "Failo pavadinimas", "file_name_with_value": "Failo pavadinimas: {file_name}", "file_size": "Failo dydis", - "filename": "Failopavadinimas", + "filename": "Failo pavadinimas", "filetype": "Failo tipas", "filter": "Filtras", "filter_description": "TiksliniÅŗ elementÅŗ filtravimo sąlygos", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "Albumo pavadinimas", "licenses": "Licencijos", "light": "Å viesi", - "like": "Kaip", - "like_deleted": "Kaip iÅĄtrintas", + "light_theme": "Perjungti į ÅĄviesią temą", + "like": "Patinka", + "like_deleted": "Patinka panaikintas", "link_motion_video": "Susieti judesio vaizdo įraÅĄÄ…", + "link_to_docs": "Daugiau informacijos rasite dokumentacijoje.", "link_to_oauth": "Susieti su OAuth", "linked_oauth_account": "Susieta OAuth paskyra", "list": "SąraÅĄas", @@ -1651,7 +1655,8 @@ "only_favorites": "Tik mėgstamiausi", "open": "Atverti", "open_calendar": "Atidaryti kalendoriÅŗ", - "open_in_map_view": "Atverti Åžemėlapio perÅžiÅĢroje", + "open_in_browser": "Atverti narÅĄyklėje", + "open_in_map_view": "Atverti Åžemėlapyje", "open_in_openstreetmap": "Atverti per OpenStreetMap", "open_the_search_filters": "Atidaryti paieÅĄkos filtrus", "options": "Pasirinktys", @@ -2212,6 +2217,7 @@ "tag": "ÅŊyma", "tag_assets": "PaÅžymėti", "tag_created": "Sukurta Åžyma: {tag}", + "tag_face": "PaÅžymėti veidą", "tag_feature_description": "PerÅžiÅĢrėkite nuotraukas ir vaizdo įraÅĄus sugrupuotus pagal suÅžymėtas temas", "tag_not_found_question": "Nerandate Åžymos? Sukurti naują Åžymą.", "tag_people": "PaÅžymėti ÅŊmones", @@ -2393,6 +2399,7 @@ "viewer_remove_from_stack": "PaÅĄalinti iÅĄ Grupės", "viewer_stack_use_as_main_asset": "Naudoti, kaip pagrindinį elementą", "viewer_unstack": "IÅĄgrupuoti", + "visibility": "Matomumas", "visibility_changed": "Matomumas pasikeitė {count, plural, one {# asmeniui} few {# asmenims} other {# asmenÅŗ}}", "visual": "IÅĄdėstymas", "visual_builder": "IÅĄdėstymo koreguotojas", diff --git a/i18n/lv.json b/i18n/lv.json index 59b5dea657..0c7776efe7 100644 --- a/i18n/lv.json +++ b/i18n/lv.json @@ -402,7 +402,7 @@ "user_settings": "Lietotāja iestatÄĢjumi", "user_settings_description": "Lietotāju iestatÄĢjumu pārvaldÄĢba", "version_check_enabled_description": "Ieslēgt versijas pārbaudi", - "version_check_implications": "Versiju pārbaudes funkcija ir atkarÄĢga no periodiskas saziņas ar github.com", + "version_check_implications": "Versiju pārbaudes funkcija ir atkarÄĢga no periodiskas saziņas ar {server}", "version_check_settings": "Versijas pārbaude", "version_check_settings_description": "Ieslēgt/izslēgt paziņojumus par jaunu versiju" }, @@ -713,9 +713,11 @@ "create_link": "Izveidot saiti", "create_link_to_share": "Izveidot kopÄĢgoÅĄanas saiti", "create_new": "IZVEIDOT JAUNU", + "create_new_face": "Izveidot jaunu seju", "create_new_person": "Izveidot jaunu personu", "create_new_person_hint": "PiesaistÄĢt izvēlētos failus jaunai personai", "create_new_user": "Izveidot jaunu lietotāju", + "create_person": "Izveidot personu", "create_shared_album_page_share_add_assets": "PIEVIENOT AKTÄĒVUS", "create_shared_album_page_share_select_photos": "Fotoattēlu Izvēle", "create_user": "Izveidot lietotāju", @@ -746,10 +748,6 @@ "day": "Diena", "days": "Dienas", "deduplicate_all": "Dedublicēt visus", - "deduplication_criteria_1": "Attēla izmēru baitos", - "deduplication_criteria_2": "EXIF datu skaitu", - "deduplication_info": "DeduplicÄ“ÅĄanas informācija", - "deduplication_info_description": "Lai automātiski atzÄĢmētu failus un masveidā noņemtu dublikātus, mēs skatāmies uz:", "delete": "Dzēst", "delete_album": "Dzēst albumu", "delete_dialog_alert": "Å ie vienumi tiks neatgriezeniski dzēsti no Immich un jÅĢsu ierÄĢces", @@ -883,6 +881,7 @@ "failed_to_update_notification_status": "Neizdevās mainÄĢt paziņojuma statusu", "incorrect_email_or_password": "Nepareizs e-pasts vai parole", "library_folder_already_exists": "Å is importa ceÄŧÅĄ jau pastāv.", + "page_not_found": "Lapa nav atrasta", "profile_picture_transparent_pixels": "Profila attēlos nevar bÅĢt caurspÄĢdÄĢgi pikseÄŧi. LÅĢdzu, palielini un/vai pārvieto attēlu.", "quota_higher_than_disk_size": "Tu esi iestatÄĢjis kvotu, kas pārsniedz diska izmēru", "something_went_wrong": "Kaut kas nogāja greizi", @@ -1299,6 +1298,7 @@ "only_favorites": "Tikai izlase", "open": "Atvērt", "open_calendar": "Atvērt kalendāru", + "open_in_browser": "Atvērt pārlÅĢkprogrammā", "open_in_map_view": "Atvērt kartes skatā", "open_in_openstreetmap": "Atvērt OpenStreetMap", "open_the_search_filters": "Atvērt meklÄ“ÅĄanas filtrus", @@ -1459,6 +1459,7 @@ "reset_people_visibility": "AtiestatÄĢt personu redzamÄĢbu", "reset_pin_code": "AtiestatÄĢt PIN kodu", "reset_sqlite": "AtiestatÄĢt SQLite datubāzi", + "reset_sqlite_clear_app_data": "NotÄĢrÄĢt datus", "reset_to_default": "AtiestatÄĢt noklusējuma iestatÄĢjumus", "resolve_duplicates": "Atrisināt dublÄ“ÅĄanās gadÄĢjumus", "resolved_all_duplicates": "Visi dublikāti ir atrisināti", @@ -1709,6 +1710,7 @@ "sync_local": "Sinhronizēt lokāli", "sync_status": "Sinhronizācijas statuss", "sync_status_subtitle": "SkatÄĢt un pārvaldÄĢt sinhronizācijas sistēmu", + "tag_face": "AtzÄĢmēt seju", "text_recognition": "Teksta atpazÄĢÅĄana", "theme": "Dizains", "theme_setting_asset_list_storage_indicator_title": "RādÄĢt krātuves indikatoru uz attēliem reÅžga skatā", @@ -1837,6 +1839,7 @@ "viewer_remove_from_stack": "Noņemt no Steka", "viewer_stack_use_as_main_asset": "Izmantot kā Galveno AktÄĢvu", "viewer_unstack": "At-Stekot", + "visibility": "RedzamÄĢba", "visual": "Vizuāli", "visual_builder": "Vizuālais veidotājs", "waiting": "Gaida", diff --git a/i18n/ml.json b/i18n/ml.json index f6d170623a..5b8a4ab7c4 100644 --- a/i18n/ml.json +++ b/i18n/ml.json @@ -420,7 +420,7 @@ "user_settings": "ā´‰ā´Ēā´¯āĩ‹ā´•āĩā´¤ā´žā´ĩā´ŋā´¨āĩā´ąāĩ† ā´•āĩā´°ā´Žāĩ€ā´•ā´°ā´Ŗā´™āĩā´™āĩž", "user_settings_description": "ā´‰ā´Ēā´¯āĩ‹ā´•āĩā´¤āĩƒ ā´•āĩā´°ā´Žāĩ€ā´•ā´°ā´Ŗā´™āĩā´™āĩž ā´•āĩˆā´•ā´žā´°āĩā´¯ā´‚ ⴚāĩ†ā´¯āĩā´¯āĩā´•", "version_check_enabled_description": "ā´Ēā´¤ā´ŋā´Ēāĩā´Ēāĩ ā´Ēā´°ā´ŋā´ļāĩ‹ā´§ā´¨ ā´Ēāĩā´°ā´ĩāĩŧā´¤āĩā´¤ā´¨ā´•āĩā´ˇā´Žā´Žā´žā´•āĩā´•āĩā´•", - "version_check_implications": "ā´Ēā´¤ā´ŋā´Ēāĩā´Ēāĩ ā´Ēā´°ā´ŋā´ļāĩ‹ā´§ā´¨ ā´Ģāĩ€ā´šāĩā´šāĩŧ github.com-ā´Žā´žā´¯ā´ŋ ⴆⴍāĩā´•ā´žā´˛ā´ŋā´• ā´†ā´ļā´¯ā´ĩā´ŋā´¨ā´ŋā´Žā´¯ā´¤āĩā´¤āĩ† ā´†ā´ļāĩā´°ā´¯ā´ŋⴚāĩā´šā´ŋā´°ā´ŋā´•āĩā´•āĩā´¨āĩā´¨āĩ", + "version_check_implications": "ā´Ēā´¤ā´ŋā´Ēāĩā´Ēāĩ ā´Ēā´°ā´ŋā´ļāĩ‹ā´§ā´¨ ā´Ģāĩ€ā´šāĩā´šāĩŧ {server}-ā´Žā´žā´¯ā´ŋ ⴆⴍāĩā´•ā´žā´˛ā´ŋā´• ā´†ā´ļā´¯ā´ĩā´ŋā´¨ā´ŋā´Žā´¯ā´¤āĩā´¤āĩ† ā´†ā´ļāĩā´°ā´¯ā´ŋⴚāĩā´šā´ŋā´°ā´ŋā´•āĩā´•āĩā´¨āĩā´¨āĩ", "version_check_settings": "ā´Ēā´¤ā´ŋā´Ēāĩā´Ēāĩ ā´Ēā´°ā´ŋā´ļāĩ‹ā´§ā´¨", "version_check_settings_description": "ā´Ēāĩā´¤ā´ŋā´¯ ā´Ēā´¤ā´ŋā´Ēāĩā´Ēā´ŋā´¨āĩā´ąāĩ† ā´…ā´ąā´ŋā´¯ā´ŋā´Ēāĩā´Ēāĩ ā´Ēāĩā´°ā´ĩāĩŧā´¤āĩā´¤ā´¨ā´•āĩā´ˇā´Žā´Žā´žā´•āĩā´•āĩā´•/ā´Ēāĩā´°ā´ĩāĩŧā´¤āĩā´¤ā´¨ā´°ā´šā´ŋā´¤ā´Žā´žā´•āĩā´•āĩā´•", "video_conversion_job": "ā´ĩāĩ€ā´Ąā´ŋā´¯āĩ‹ā´•āĩž ⴟāĩā´°ā´žāĩģā´¸āĩâ€Œā´•āĩ‹ā´Ąāĩ ⴚāĩ†ā´¯āĩā´¯āĩā´•", @@ -822,10 +822,6 @@ "day": "ā´Ļā´ŋā´ĩⴏⴂ", "days": "ā´Ļā´ŋā´ĩⴏⴙāĩā´™āĩž", "deduplicate_all": "ā´Žā´˛āĩā´˛ā´ž ā´Ąāĩā´¯āĩ‚ā´Ēāĩā´˛ā´ŋā´•āĩā´•āĩ‡ā´ąāĩā´ąāĩā´•ā´ŗāĩā´‚ ā´’ā´´ā´ŋā´ĩā´žā´•āĩā´•āĩā´•", - "deduplication_criteria_1": "ⴚā´ŋā´¤āĩā´°ā´¤āĩā´¤ā´ŋā´¨āĩā´ąāĩ† ā´ĩā´˛āĩā´Ēāĩā´Ēā´‚ (ā´Ŧāĩˆā´ąāĩā´ąāĩā´•ā´ŗā´ŋāĩŊ)", - "deduplication_criteria_2": "EXIF ā´Ąā´žā´ąāĩā´ąā´¯āĩā´Ÿāĩ† ā´Žā´Ŗāĩā´Ŗā´‚", - "deduplication_info": "ā´Ąāĩā´¯āĩ‚ā´Ēāĩā´˛ā´ŋā´•āĩā´•āĩ‡ā´ˇāĩģ ā´’ā´´ā´ŋā´ĩā´žā´•āĩā´•āĩŊ ā´ĩā´ŋā´ĩā´°ā´‚", - "deduplication_info_description": "ā´…ā´¸ā´ąāĩā´ąāĩā´•āĩž ā´¯ā´žā´¨āĩā´¤āĩā´°ā´ŋā´•ā´Žā´žā´¯ā´ŋ ā´Žāĩāĩģā´•āĩ‚ā´Ÿāĩā´Ÿā´ŋ ā´¤ā´ŋā´°ā´žāĩā´žāĩ†ā´Ÿāĩā´•āĩā´•āĩā´¨āĩā´¨ā´¤ā´ŋā´¨āĩā´‚ ā´Ąāĩā´¯āĩ‚ā´Ēāĩā´˛ā´ŋā´•āĩā´•āĩ‡ā´ąāĩā´ąāĩā´•āĩž ā´Ŧāĩžā´•āĩā´•ā´žā´¯ā´ŋ ā´¨āĩ€ā´•āĩā´•ā´‚ ⴚāĩ†ā´¯āĩā´¯āĩā´¨āĩā´¨ā´¤ā´ŋā´¨āĩā´‚, ā´žā´™āĩā´™āĩž ā´‡ā´ĩ ā´Ēā´°ā´ŋā´—ā´Ŗā´ŋā´•āĩā´•āĩā´¨āĩā´¨āĩ:", "delete": "ⴇⴞāĩā´˛ā´žā´¤ā´žā´•āĩā´•āĩā´•", "delete_action_confirmation_message": "ⴈ ā´…ā´¸ā´ąāĩā´ąāĩ ⴇⴞāĩā´˛ā´žā´¤ā´žā´•āĩā´•ā´Ŗā´Žāĩ†ā´¨āĩā´¨āĩ ā´¨ā´ŋā´™āĩā´™āĩžā´•āĩā´•āĩ ā´‰ā´ąā´Ēāĩā´Ēā´žā´Ŗāĩ‹? ⴈ ā´Ēāĩā´°ā´ĩāĩŧā´¤āĩā´¤ā´¨ā´‚ ā´…ā´¸ā´ąāĩā´ąā´ŋā´¨āĩ† ā´¸āĩ†āĩŧā´ĩā´ąā´ŋā´¨āĩā´ąāĩ† ⴟāĩā´°ā´žā´ˇā´ŋā´˛āĩ‡ā´•āĩā´•āĩ ā´Žā´žā´ąāĩā´ąāĩā´‚, ā´•āĩ‚ā´Ÿā´žā´¤āĩ† ⴇⴤāĩ ā´Ēāĩā´°ā´žā´Ļāĩ‡ā´ļā´ŋā´•ā´Žā´žā´¯ā´ŋ ⴇⴞāĩā´˛ā´žā´¤ā´žā´•āĩā´•ā´Ŗāĩ‹ ā´Žā´¨āĩā´¨āĩ ⴚāĩ‹ā´Ļā´ŋā´•āĩā´•āĩā´•ā´¯āĩā´‚ ⴚāĩ†ā´¯āĩā´¯āĩā´‚", "delete_action_prompt": "{count} ā´Žā´Ŗāĩā´Ŗā´‚ ⴇⴞāĩā´˛ā´žā´¤ā´žā´•āĩā´•ā´ŋ", diff --git a/i18n/mr.json b/i18n/mr.json index cbeac5131f..8b6244b94e 100644 --- a/i18n/mr.json +++ b/i18n/mr.json @@ -408,7 +408,7 @@ "user_settings": "ā¤ĩā¤žā¤Ē⤰⤕⤰āĨā¤¤ā¤ž ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤œ", "user_settings_description": "ā¤ĩā¤žā¤Ē⤰⤕⤰āĨā¤¤ā¤ž ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤œ ā¤ĩāĨā¤¯ā¤ĩ⤏āĨā¤Ĩā¤žā¤Ēā¤ŋ⤤ ā¤•ā¤°ā¤ž", "version_check_enabled_description": "⤆ā¤ĩāĨƒā¤¤āĨā¤¤āĨ€ ⤤ā¤Ēā¤žā¤¸ā¤ŖāĨ€ ⤏⤕āĨā¤ˇā¤Ž ā¤•ā¤°ā¤ž", - "version_check_implications": "⤆ā¤ĩāĨƒā¤¤āĨā¤¤āĨ€ ⤤ā¤Ēā¤žā¤¸ā¤ŖāĨ€ ā¤ĩāĨˆā¤ļā¤ŋ⤎āĨā¤ŸāĨā¤¯ GitHub.com ⤏āĨ‹ā¤Ŧ⤤ ⤆ā¤ĩ⤰āĨā¤¤āĨ€ ⤏⤂ā¤ĩā¤žā¤Ļā¤žā¤ĩ⤰ ⤅ā¤ĩ⤞⤂ā¤ŦāĨ‚⤍ ā¤†ā¤šāĨ‡", + "version_check_implications": "⤆ā¤ĩāĨƒā¤¤āĨā¤¤āĨ€ ⤤ā¤Ēā¤žā¤¸ā¤ŖāĨ€ ā¤ĩāĨˆā¤ļā¤ŋ⤎āĨā¤ŸāĨā¤¯ {server} ⤏āĨ‹ā¤Ŧ⤤ ⤆ā¤ĩ⤰āĨā¤¤āĨ€ ⤏⤂ā¤ĩā¤žā¤Ļā¤žā¤ĩ⤰ ⤅ā¤ĩ⤞⤂ā¤ŦāĨ‚⤍ ā¤†ā¤šāĨ‡", "version_check_settings": "⤆ā¤ĩāĨƒā¤¤āĨā¤¤āĨ€ ⤤ā¤Ēā¤žā¤¸ā¤ŖāĨ€", "version_check_settings_description": "⤍ā¤ĩāĨ€ā¤¨ ⤆ā¤ĩāĨƒā¤¤āĨā¤¤āĨ€ ⤏āĨ‚ā¤šā¤¨ā¤ž ⤏⤕āĨā¤ˇā¤Ž/⤅⤕āĨā¤ˇā¤Ž ā¤•ā¤°ā¤ž", "video_conversion_job": "ā¤ĩāĨā¤šā¤ŋā¤Ąā¤ŋ⤓ ⤟āĨā¤°ā¤žā¤¨āĨā¤¸ā¤•āĨ‹ā¤Ą ā¤•ā¤°ā¤ž", @@ -810,10 +810,6 @@ "day": "ā¤Ļā¤ŋā¤ĩ⤏", "days": "⤅⤍āĨ‡ā¤• ā¤Ļā¤ŋā¤ĩ⤏", "deduplicate_all": "⤏⤰āĨā¤ĩ ā¤ĄāĨā¤ĒāĨā¤˛ā¤ŋ⤕āĨ‡ā¤Ÿ ā¤•ā¤žā¤ĸā¤ž", - "deduplication_criteria_1": "ā¤ĒāĨā¤°ā¤¤ā¤ŋā¤ŽāĨ‡ā¤šā¤ž ā¤†ā¤•ā¤žā¤° (ā¤Ŧā¤žā¤‡ā¤ŸāĨā¤¸)", - "deduplication_criteria_2": "EXIF ā¤ĄāĨ‡ā¤Ÿā¤ž ā¤ĒāĨā¤°ā¤Žā¤žā¤Ŗ", - "deduplication_info": "ā¤ĄāĨā¤ĒāĨā¤˛ā¤ŋ⤕āĨ‡ā¤Ÿ ⤍ā¤ŋā¤ĩā¤žā¤°ā¤Ŗ ā¤Žā¤žā¤šā¤ŋ⤤āĨ€", - "deduplication_info_description": "ā¤ĄāĨā¤ĒāĨā¤˛ā¤ŋ⤕āĨ‡ā¤Ÿ ⤏āĨā¤ĩā¤¯ā¤‚ā¤šā¤˛ā¤ŋ⤤ā¤Ē⤪āĨ‡ ⤍ā¤ŋā¤ĩā¤ĄāĨ‚⤍ ā¤•ā¤žā¤ĸ⤪āĨā¤¯ā¤žā¤¸ā¤žā¤ āĨ€ ā¤–ā¤žā¤˛āĨ€ā¤˛ ⤍ā¤ŋ⤕⤎ ā¤ĩā¤žā¤Ē⤰⤞āĨ‡ ā¤œā¤žā¤¤ā¤žā¤¤:", "delete": "ā¤šā¤Ÿā¤ĩā¤ž", "delete_action_confirmation_message": "⤤āĨā¤ŽāĨā¤šā¤žā¤˛ā¤ž ā¤šāĨ€ ā¤Ģā¤žā¤ˆā¤˛ ā¤šā¤Ÿā¤ĩā¤žā¤¯ā¤šāĨ€ ā¤†ā¤šāĨ‡ ā¤•ā¤ž? ā¤šāĨ€ ⤕āĨā¤°ā¤ŋā¤¯ā¤ž ⤏⤰āĨā¤ĩāĨā¤šā¤°ā¤šāĨā¤¯ā¤ž ⤟āĨā¤°āĨ…ā¤ļā¤Žā¤§āĨā¤¯āĨ‡ ā¤šā¤˛ā¤ĩāĨ‡ā¤˛ ⤆⤪ā¤ŋ ⤏āĨā¤Ĩā¤žā¤¨ā¤ŋ⤕ā¤Ē⤪āĨ‡ ā¤šā¤Ÿā¤ĩā¤žā¤¯ā¤šāĨ‡ ā¤•ā¤ž ⤤āĨ‡ ā¤ĩā¤ŋā¤šā¤žā¤°āĨ‡ā¤˛", "delete_action_prompt": "{count} ā¤šā¤Ÿā¤ĩ⤞āĨ‡", diff --git a/i18n/ms.json b/i18n/ms.json index 0c1ae6c156..5459b78450 100644 --- a/i18n/ms.json +++ b/i18n/ms.json @@ -5,6 +5,7 @@ "acknowledge": "Akui", "action": "Tindakan", "action_common_update": "Kemaskini", + "action_description": "Satu set tindakan untuk dilakukan atas aset yang ditapis", "actions": "Tindakan", "active": "Aktif", "active_count": "Aktif: {count}", @@ -16,6 +17,7 @@ "add_a_name": "Tambah nama", "add_a_title": "Tambah tajuk", "add_action": "Tambah Tindakan", + "add_assets": "Tambah aset", "add_birthday": "Tambah hari jadi", "add_endpoint": "Tambah titik akhir", "add_exclusion_pattern": "Tambahkan corak pengecualian", @@ -393,7 +395,7 @@ "user_settings": "Tetapan Pengguna", "user_settings_description": "Urus tetapan pengguna", "version_check_enabled_description": "Dayakan semakan versi", - "version_check_implications": "Ciri semakan versi bergantung kepada komunikasi berkala dengan github.com", + "version_check_implications": "Ciri semakan versi bergantung kepada komunikasi berkala dengan {server}", "version_check_settings": "Semakan Versi", "version_check_settings_description": "Dayakan/nyahdayakan notifikasi versi baharu", "video_conversion_job": "Transkod video", @@ -433,10 +435,6 @@ "album_user_left": "Kiri {album}", "album_user_removed": "{user} telah dibuang", "album_with_link_access": "Benarkan sesiapa yang mempunyai pautan melihat foto dan individu dalam album ini.", - "deduplication_criteria_1": "Saiz imej dalam bait", - "deduplication_criteria_2": "Kiraan data EXIF", - "deduplication_info": "Maklumat Pendeduplikasian", - "deduplication_info_description": "Untuk prapilih aset secara automatik dan mengalih keluar pendua secara pukal, kami melihat pada:", "delete": "Padam", "delete_album": "Padam album", "delete_api_key_prompt": "Adakah anda pasti mahu memadam kunci API ini?", diff --git a/i18n/nb_NO.json b/i18n/nb_NO.json index 4de7864811..1602707dd9 100644 --- a/i18n/nb_NO.json +++ b/i18n/nb_NO.json @@ -5,7 +5,7 @@ "acknowledge": "Bekreft", "action": "Handling", "action_common_update": "Oppdater", - "action_description": "Ett sett med handlinger som skal utføres pÃĨ de filtrerede objekter", + "action_description": "Ett sett handlinger som skal utføres pÃĨ de filtrerte mediefilene", "actions": "Handlinger", "active": "Aktiv", "active_count": "Aktiv: {count}", @@ -18,7 +18,7 @@ "add_a_title": "Legg til tittel", "add_action": "Legg til hendelse", "add_action_description": "Trykk for ÃĨ legge til en hendelse ÃĨ utføre", - "add_assets": "Legg til objekter", + "add_assets": "Legg til mediefiler", "add_birthday": "Legg til bursdag", "add_endpoint": "Legg til endepunkt", "add_exclusion_pattern": "Legg til ekskluderingsmønster", @@ -34,7 +34,7 @@ "add_to_album": "Legg til album", "add_to_album_bottom_sheet_added": "Lagt til i {album}", "add_to_album_bottom_sheet_already_exists": "Allerede i {album}", - "add_to_album_bottom_sheet_some_local_assets": "Noen lokale elementer kunne ikke legges til i albumet", + "add_to_album_bottom_sheet_some_local_assets": "Noen lokale filer kunne ikke legges til i albumet", "add_to_album_toggle": "Avhuking for {album}", "add_to_albums": "Legg til i album", "add_to_albums_count": "Legg til i album ({count})", @@ -50,8 +50,8 @@ "add_exclusion_pattern_description": "Legg til ekskluderingsmønstre. Globbing med *, ** og ? støttes. For ÃĨ ignorere alle filer i en hvilken som helst mappe som heter \"Raw\", bruk \"**/Raw/**\". For ÃĨ ignorere alle filer som slutter pÃĨ \".tif\", bruk \"**/*.tif\". For ÃĨ ignorere en absolutt filplassering, bruk \"/filsti/til/ignorer/**\".", "admin_user": "Administrasjonsbruker", "asset_offline_description": "Dette eksterne bibliotekselementet finnes ikke lenger pÃĨ disk og har blitt flyttet til papirkurven. Hvis filen ble flyttet innad i biblioteket, se etter det tilsvarende elementet i tidslinjen din. For ÃĨ gjenopprette elementet, vennligst sørg for at filstien under er tilgjengelig for Immich og skann biblioteket.", - "authentication_settings": "Godkjenninger", - "authentication_settings_description": "Administrer passord, OAuth, og andre innstillinger for autentisering", + "authentication_settings": "Godkjenings Instillinger", + "authentication_settings_description": "Administrer passord, OAuth, og andre innstillinger for autentiserings Instilinger", "authentication_settings_disable_all": "Er du sikker pÃĨ at du ønsker ÃĨ deaktivere alle innloggingsmetoder? Innlogging vil bli fullstendig deaktivert.", "authentication_settings_reenable": "For ÃĨ aktivere pÃĨ nytt, bruk en Server Command.", "background_task_job": "Bakgrunnsjobber", @@ -81,7 +81,7 @@ "cron_expression_description": "Still inn skanneintervallet med cron-formatet. For mer informasjon henvises til f.eks. Crontab Guru", "cron_expression_presets": "ForhÃĨndsinnstillinger for Cron-uttrykk", "disable_login": "Deaktiver innlogging", - "duplicate_detection_job_description": "Kjør maskinlÃĻring pÃĨ filer for ÃĨ oppdage lignende bilder. Krever bruk av Smart Search", + "duplicate_detection_job_description": "Kjør maskinlÃĻring pÃĨ filer for ÃĨ oppdage lignende bilder. Krever bruk av Smart Søk", "exclusion_pattern_description": "Ekskluderingsmønstre lar deg ignorere filer og mapper nÃĨr du skanner biblioteket ditt. Dette er nyttig hvis du har mapper som inneholder filer du ikke vil importere, for eksempel RAW-filer.", "export_config_as_json_description": "Last ned nÃĨvÃĻrende systemkonfigurasjon som en JSON fil", "external_libraries_page_description": "Administrering for eksterne bibliotek", @@ -441,7 +441,7 @@ "user_successfully_removed": "Bruker {email} har blitt fjernet.", "users_page_description": "Administrer brukere", "version_check_enabled_description": "Aktiver periodiske forespørsler til GitHub for ÃĨ sjekke etter nye utgivelser", - "version_check_implications": "Versjonssjekkfunksjonen baserer seg pÃĨ periodisk kommunikasjon med github.com", + "version_check_implications": "Versjonssjekkfunksjonen baserer seg pÃĨ periodisk kommunikasjon med {server}", "version_check_settings": "Versjonssjekk", "version_check_settings_description": "Aktiver/deaktiver varsel om ny versjon", "video_conversion_job": "Transkod videoer", @@ -849,9 +849,12 @@ "create_link_to_share": "Opprett delelink", "create_link_to_share_description": "La alle med lenken se de(t) valgte bildet/bildene", "create_new": "LAG NY", + "create_new_face": "Opprett nytt ansikt", "create_new_person": "Opprett ny person", "create_new_person_hint": "Tildel valgte eiendeler til en ny person", "create_new_user": "Opprett ny bruker", + "create_person": "Opprett person", + "create_person_subtitle": "Gi det valgte ansiktet et navn for ÃĨ opprette og tagge den nye personen", "create_shared_album_page_share_add_assets": "LEGG TIL OBJEKTER", "create_shared_album_page_share_select_photos": "Velg bilder", "create_shared_link": "Opprett delt lenke", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "Fikset", "crop_aspect_ratio_free": "Lagret", "crop_aspect_ratio_original": "Original", + "crop_aspect_ratio_square": "Firkant", "curated_object_page_title": "Ting", "current_device": "NÃĨvÃĻrende enhet", "current_pin_code": "NÃĨvÃĻrende PIN kode", @@ -880,7 +884,7 @@ "daily_title_text_date": "E MMM. dd", "daily_title_text_date_year": "E MMM. dddd, yyyy", "dark": "Mørk", - "dark_theme": "Aktiver mørk-modus", + "dark_theme": "Skift til mørkt tema", "date": "Dato", "date_after": "Dato etter", "date_and_time": "Dato og tid", @@ -891,12 +895,10 @@ "day": "Dag", "days": "Dager", "deduplicate_all": "De-dupliser alle", - "deduplication_criteria_1": "Bilde størrelse i bytes", - "deduplication_criteria_2": "Antall av EXIF data", - "deduplication_info": "Dedupliseringsinformasjon", - "deduplication_info_description": "For ÃĨ automatisk forhÃĨndsvelge eiendeler og fjerne duplikater samtidig, ser vi pÃĨ:", + "default_locale": "StandardsprÃĨk", + "default_locale_description": "Formater datoer og tall basert pÃĨ din nettlesers sprÃĨkinnstillinger", "delete": "Slett", - "delete_action_confirmation_message": "Vil du virkelig slette dette elementet? Dette vil flytte elementet til papirkurvn og vil gi deg beskjed om du vil slette det lokalt", + "delete_action_confirmation_message": "Vil du virkelig slette dette elementet? Dette vil flytte elementet til papirkurven og vil gi deg beskjed om du vil slette det lokalt", "delete_action_prompt": "{count} slettet", "delete_album": "Slett album", "delete_api_key_prompt": "Vil du virkelig slette denne API-nøkkelen?", @@ -970,7 +972,7 @@ "downloading_media": "Laster ned media", "drop_files_to_upload": "Slipp filer hvor som helst for ÃĨ laste opp", "duplicates": "Duplikater", - "duplicates_description": "Løs hver gruppe ved ÃĨ angi hvilke, hvis noen, er duplikater", + "duplicates_description": "Løs hver gruppe ved ÃĨ angi hvilke, hvis noen, er duplikater.", "duration": "Varighet", "edit": "Rediger", "edit_album": "Rediger album", @@ -1007,8 +1009,8 @@ "editor_edits_applied_success": "Lagring av endringer vellykket", "editor_flip_horizontal": "Roter horisontalt", "editor_flip_vertical": "Roter vertikalt", - "editor_handle_corner": "{corner, select, top_left {Øvre venstre} top_right {Øvre høyre} bottom_left {Nedre venstre} bottom_right {Nedre høyre} other {A}} hjørnehÃĨndtak", - "editor_handle_edge": "{edge, select, top {Øvre} bottom {Nedre} left {Venstre} right {Høyre} other {Et}} kanthÃĨndtak", + "editor_handle_corner": "{corner, select, top_left {Øverst venstre} top_right {Øverst høyre} bottom_left {Nederst venstre} bottom_right {Nederst høyre} other {A}} hjørnehÃĨndtak", + "editor_handle_edge": "{edge, select, top {Øverst} bottom {Nederst} left {Venstre} right {Høyre} other {Et}} kanthÃĨndtak", "editor_orientation": "Orientering", "editor_reset_all_changes": "Tilbakestill endringer", "editor_rotate_left": "Roter 90° mot klokken", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "Albumtittel", "licenses": "Lisenser", "light": "Lys", + "light_theme": "Skift til lyst tema", "like": "Lik", "like_deleted": "Som slettede", "link_motion_video": "Koble bevegelsesvideo", + "link_to_docs": "For mer informasjon, se dokumentasjonen.", "link_to_oauth": "Lenke til OAuth", "linked_oauth_account": "Lenket til OAuth-konto", "list": "Liste", @@ -1651,6 +1655,7 @@ "only_favorites": "Bare favoritter", "open": "Åpne", "open_calendar": "Åpne kalender", + "open_in_browser": "Åpne i nettleser", "open_in_map_view": "Åpne i kartvisning", "open_in_openstreetmap": "Åpne i OpenStreetMap", "open_the_search_filters": "Åpne søkefiltrene", @@ -1719,9 +1724,9 @@ "permission_onboarding_permission_limited": "Begrenset tilgang. For ÃĨ la Immich sikkerhetskopiere og hÃĨndtere galleriet, tillatt bilde- og video-tilgang i Innstillinger.", "permission_onboarding_request": "Immich trenger tilgang til ÃĨ se dine bilder og videoer.", "person": "Person", - "person_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_age_months": "{months, plural, one {# mÃĨned} other {# mÃĨneder}} gammel", + "person_age_year_months": "1 ÃĨr, {months, plural, one {# mÃĨned} other {# mÃĨneder}} gammel", + "person_age_years": "{years, plural, other {# ÃĨr}} gammel", "person_birthdate": "Født den {date}", "person_hidden": "{name}{hidden, select, true { (skjult)} other {}}", "person_recognized": "Person gjenkjent", @@ -2212,6 +2217,7 @@ "tag": "Tagg", "tag_assets": "Merk ressurser", "tag_created": "Lag merke: {tag}", + "tag_face": "Tagg ansikt", "tag_feature_description": "Bla gjennom bilder og videoer gruppert etter logiske merke-emner", "tag_not_found_question": "Finner du ikke en merke? Opprett en nytt merke.", "tag_people": "Tag personer", @@ -2393,6 +2399,7 @@ "viewer_remove_from_stack": "Fjern fra stabling", "viewer_stack_use_as_main_asset": "Bruk som hovedelement", "viewer_unstack": "avstable", + "visibility": "Synlighet", "visibility_changed": "Synlighet endret for {count, plural, one {# person} other {# people}}", "visual": "Visuell", "visual_builder": "Visuell oppbygging", diff --git a/i18n/nl.json b/i18n/nl.json index 89daa4bee5..c584fc4b86 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -349,7 +349,7 @@ "template_email_update_album": "Update in album sjabloon", "template_email_welcome": "Welkomstmail sjabloon", "template_settings": "Melding sjablonen", - "template_settings_description": "Beheer aangepast sjablonen voor meldingen", + "template_settings_description": "Beheer aangepaste sjablonen voor meldingen", "theme_custom_css_settings": "Aangepaste CSS", "theme_custom_css_settings_description": "Met Cascading Style Sheets kan het ontwerp van Immich worden aangepast.", "theme_settings": "Thema-instellingen", @@ -441,7 +441,7 @@ "user_successfully_removed": "Gebruiker {email} is succesvol verwÄŗderd.", "users_page_description": "Gebruikers­pagina voor administrators", "version_check_enabled_description": "Versiecontrole inschakelen", - "version_check_implications": "De versiecontrole is afhankelijk van periodieke communicatie met github.com", + "version_check_implications": "De versiecontrole is afhankelijk van periodieke communicatie met {server}", "version_check_settings": "Versiecontrole", "version_check_settings_description": "Melding voor een nieuwe versie in-/uitschakelen", "video_conversion_job": "Transcodeer video's", @@ -544,7 +544,7 @@ "appears_in": "Komt voor in", "apply_count": "Toepassen ({count, number})", "archive": "Archief", - "archive_action_prompt": "{count} item(s) toegevoegd aan het archief", + "archive_action_prompt": "{count, plural, one {# item} other {# items}} toegevoegd aan het archief", "archive_or_unarchive_photo": "Foto archiveren of uit het archief halen", "archive_page_no_archived_assets": "Geen gearchiveerde items gevonden", "archive_page_title": "Archief ({count})", @@ -593,20 +593,20 @@ "assets_cannot_be_added_to_album_count": "{count, plural, one {# item} other {# items}} konden niet aan album toegevoegd worden", "assets_cannot_be_added_to_albums": "{count, plural, one {Item kan} other {Items kunnen}} niet toegevoegd worden aan de albums", "assets_count": "{count, plural, one {# item} other {# items}}", - "assets_deleted_permanently": "{count} item(s) permanent verwijderd", - "assets_deleted_permanently_from_server": "{count} item(s) permanent verwijderd van de Immich server", + "assets_deleted_permanently": "{count, plural, one {# item} other {# items}} permanent verwijderd", + "assets_deleted_permanently_from_server": "{count, plural, one {# item} other {# items}} permanent verwijderd van de Immich server", "assets_downloaded_failed": "{count, plural, one {# bestand gedownload - {error} bestand mislukt} other {# bestanden gedownload - {error} bestanden mislukt}}", "assets_downloaded_successfully": "{count, plural, one {# bestand succesvol gedownload} other {# bestanden succesvol gedownload}}", "assets_moved_to_trash_count": "{count, plural, one {# item} other {# items}} verplaatst naar prullenbak", "assets_permanently_deleted_count": "{count, plural, one {# item} other {# items}} permanent verwijderd", "assets_removed_count": "{count, plural, one {# item} other {# items}} verwijderd", - "assets_removed_permanently_from_device": "{count} item(s) permanent verwijderd van je apparaat", + "assets_removed_permanently_from_device": "{count, plural, one {# item} other {# items}} permanent verwijderd van je apparaat", "assets_restore_confirmation": "Weet je zeker dat je alle verwijderde items wilt herstellen? Je kunt deze actie niet ongedaan maken! Offline items kunnen op deze manier niet worden hersteld.", "assets_restored_count": "{count, plural, one {# item} other {# items}} hersteld", - "assets_restored_successfully": "{count} item(s) succesvol hersteld", - "assets_trashed": "{count} item(s) naar de prullenbak verplaatst", + "assets_restored_successfully": "{count, plural, one {# item} other {# items}} succesvol hersteld", + "assets_trashed": "{count, plural, one {# item} other {# items}} naar de prullenbak verplaatst", "assets_trashed_count": "{count, plural, one {# item} other {# items}} naar prullenbak verplaatst", - "assets_trashed_from_server": "{count} item(s) naar de prullenbak verplaatst op de Immich server", + "assets_trashed_from_server": "{count, plural, one {# item} other {# items}} naar de prullenbak verplaatst op de Immich server", "assets_were_part_of_album_count": "{count, plural, one {Item was} other {Items waren}} al onderdeel van het album", "assets_were_part_of_albums_count": "{count, plural, one {Item is} other {Items zijn}} al onderdeel van de albums", "authorized_devices": "Geautoriseerde apparaten", @@ -849,9 +849,12 @@ "create_link_to_share": "Gedeelde link maken", "create_link_to_share_description": "Laat iedereen met de link de geselecteerde foto(s) zien", "create_new": "MAAK NIEUW", + "create_new_face": "Nieuw gezicht aanmaken", "create_new_person": "Nieuwe persoon aanmaken", "create_new_person_hint": "Geselecteerde items toewijzen aan een nieuwe persoon", "create_new_user": "Nieuwe gebruiker aanmaken", + "create_person": "Persoon aanmaken", + "create_person_subtitle": "Voeg een naam toe aan het geselecteerde gezicht om de nieuwe persoon aan te maken en te taggen", "create_shared_album_page_share_add_assets": "ITEMS TOEVOEGEN", "create_shared_album_page_share_select_photos": "Selecteer foto's", "create_shared_link": "Gedeelde link maken", @@ -866,21 +869,22 @@ "crop_aspect_ratio_fixed": "Vast", "crop_aspect_ratio_free": "Vrij", "crop_aspect_ratio_original": "Origineel", + "crop_aspect_ratio_square": "Vierkant", "curated_object_page_title": "Dingen", "current_device": "Huidig apparaat", "current_pin_code": "Huidige pincode", "current_server_address": "Huidig serveradres", "custom_date": "Aangepaste datum", "custom_locale": "Aangepaste landinstelling", - "custom_locale_description": "Formatteer datums, tijden en getallen op basis van de geselecteerde taal en de regio", + "custom_locale_description": "Formatteer datums, tijden, en getallen op basis van de geselecteerde taal en regio", "custom_url": "Aangepaste URL", "cutoff_date_description": "Bewaar foto's van de laatsteâ€Ļ", "cutoff_day": "{count, plural, one {dag} other {dagen}}", - "cutoff_year": "{count, plural, one {jaar} other {jaar}}", + "cutoff_year": "{count, plural, one {jaar} other {jaren}}", "daily_title_text_date": "E dd MMM", "daily_title_text_date_year": "E dd MMM yyyy", "dark": "Donker", - "dark_theme": "Donker thema in- of uitschakelen", + "dark_theme": "Wissel naar donker thema", "date": "Datum", "date_after": "Datum na", "date_and_time": "Datum en tijd", @@ -891,13 +895,11 @@ "day": "Dag", "days": "Dagen", "deduplicate_all": "Alles dedupliceren", - "deduplication_criteria_1": "Grootte van afbeelding in bytes", - "deduplication_criteria_2": "Aantal EXIF data", - "deduplication_info": "Deduplicatie-info", - "deduplication_info_description": "Om automatisch items te preselecteren en duplicaten te verwijderen in bulk, kijken we naar:", + "default_locale": "Standaard landinstelling", + "default_locale_description": "Formatteer datums en getallen op basis van de taalinstellingen van je browser", "delete": "Verwijderen", "delete_action_confirmation_message": "Weet je zeker dat je dit item wilt verwijderen? Deze actie zorgt ervoor dat het item naar de prullenbak van de server wordt verplaatst en je wordt gevraagd of je deze ook lokaal wilt verwijderen", - "delete_action_prompt": "{count} item(s) verwijderd", + "delete_action_prompt": "{count} verwijderd", "delete_album": "Album verwijderen", "delete_api_key_prompt": "Weet je zeker dat je deze API-sleutel wilt verwijderen?", "delete_dialog_alert": "Deze items zullen permanent verwijderd worden van Immich en je apparaat", @@ -911,12 +913,12 @@ "delete_key": "Verwijder key", "delete_library": "Verwijder bibliotheek", "delete_link": "Verwijder link", - "delete_local_action_prompt": "{count} item(s) lokaal verwijderd", + "delete_local_action_prompt": "{count} lokaal verwijderd", "delete_local_dialog_ok_backed_up_only": "Verwijder alleen met back-up", "delete_local_dialog_ok_force": "Toch verwijderen", "delete_others": "Andere verwijderen", "delete_permanently": "Permanent verwijderen", - "delete_permanently_action_prompt": "{count} item(s) permanent verwijderd", + "delete_permanently_action_prompt": "{count} permanent verwijderd", "delete_shared_link": "Verwijder gedeelde link", "delete_shared_link_dialog_title": "Verwijder gedeelde link", "delete_tag": "Tag verwijderen", @@ -946,7 +948,7 @@ "documentation": "Documentatie", "done": "Klaar", "download": "Downloaden", - "download_action_prompt": "{count} item(s) aan het downloaden", + "download_action_prompt": "{count, plural, one {# item} other {# items}} aan het downloaden", "download_canceled": "Download geannuleerd", "download_complete": "Download voltooid", "download_enqueue": "Download in wachtrij", @@ -970,7 +972,7 @@ "downloading_media": "Media aan het downloaden", "drop_files_to_upload": "Zet bestanden ergens neer om ze te uploaden", "duplicates": "Duplicaten", - "duplicates_description": "Kies voor iedere groep welke, indien aanwezig, duplicaten zijn", + "duplicates_description": "Kies voor iedere groep welke, indien aanwezig, duplicaten zijn.", "duration": "Tijdsduur", "edit": "Bewerken", "edit_album": "Album bewerken", @@ -978,7 +980,7 @@ "edit_birthday": "Wijzig verjaardag", "edit_date": "Datum bewerken", "edit_date_and_time": "Datum en tijd bewerken", - "edit_date_and_time_action_prompt": "Datum en tijd bijgewerkt van {count} item(s)", + "edit_date_and_time_action_prompt": "Datum en tijd bijgewerkt van {count, plural, one {# item} other {# items}}", "edit_date_and_time_by_offset": "Wijzigen datum door verschuiving", "edit_date_and_time_by_offset_interval": "Nieuw datuminterval: {from}-{to}", "edit_description": "Beschrijving bewerken", @@ -988,7 +990,7 @@ "edit_key": "Key bewerken", "edit_link": "Link bewerken", "edit_location": "Locatie bewerken", - "edit_location_action_prompt": "Locatie bijgewerkt van {count} item(s)", + "edit_location_action_prompt": "Locatie bijgewerkt van {count, plural, one {# item} other {# items}}", "edit_location_dialog_title": "Locatie", "edit_name": "Naam bewerken", "edit_people": "Mensen bewerken", @@ -1201,7 +1203,7 @@ "failed_to_load_assets": "Kan items niet laden", "failed_to_load_folder": "Laden van map mislukt", "favorite": "Favoriet", - "favorite_action_prompt": "{count} item(s) toegevoegd aan je favorieten", + "favorite_action_prompt": "{count, plural, one {# item} other {# items}} toegevoegd aan je favorieten", "favorite_or_unfavorite_photo": "Foto markeren als of verwijderen uit favorieten", "favorites": "Favorieten", "favorites_page_no_favorites": "Geen favoriete items gevonden", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "Albumtitel", "licenses": "Licenties", "light": "Licht", + "light_theme": "Wissel naar licht thema", "like": "Vind ik leuk", "like_deleted": "Like verwijderd", "link_motion_video": "Koppel bewegende video", + "link_to_docs": "Raadpleeg voor meer informatie de documentatie.", "link_to_oauth": "Koppel OAuth", "linked_oauth_account": "Gekoppeld OAuth account", "list": "Lijst", @@ -1547,7 +1551,7 @@ "move_off_locked_folder": "Verplaats uit vergrendelde map", "move_to": "Verplaatsen naar", "move_to_device_trash": "Naar prullenbak van apparaat", - "move_to_lock_folder_action_prompt": "{count} item(s) toegevoegd aan de vergrendelde map", + "move_to_lock_folder_action_prompt": "{count, plural, one {# item} other {# items}} toegevoegd aan de vergrendelde map", "move_to_locked_folder": "Verplaats naar vergrendelde map", "move_to_locked_folder_confirmation": "Deze foto’s en video’s worden uit alle albums verwijderd en zijn alleen te bekijken in de vergrendelde map", "move_up": "Naar boven verplaatsen", @@ -1850,9 +1854,9 @@ "remove_custom_date_range": "Aangepast datumbereik verwijderen", "remove_deleted_assets": "Verwijder offline bestanden", "remove_from_album": "Verwijderen uit album", - "remove_from_album_action_prompt": "{count} item(s) verwijderd uit het album", + "remove_from_album_action_prompt": "{count, plural, one {# item} other {# items}} verwijderd uit het album", "remove_from_favorites": "Verwijderen uit favorieten", - "remove_from_lock_folder_action_prompt": "{count} item(s) verwijderd uit de vergrendelde map", + "remove_from_lock_folder_action_prompt": "{count, plural, one {# item} other {# items}} verwijderd uit de vergrendelde map", "remove_from_locked_folder": "Verwijder uit de vergrendelde map", "remove_from_locked_folder_confirmation": "Weet je zeker dat je deze foto's en video's uit de vergrendelde map wilt verplaatsen? Ze zijn dan weer zichtbaar in je bibliotheek.", "remove_from_shared_link": "Verwijderen uit gedeelde link", @@ -1895,7 +1899,7 @@ "resolved_all_duplicates": "Alle duplicaten opgelost", "restore": "Herstellen", "restore_all": "Herstel alle", - "restore_trash_action_prompt": "{count} item(s) teruggehaald uit de prullenbak", + "restore_trash_action_prompt": "{count, plural, one {# item} other {# items}} teruggehaald uit de prullenbak", "restore_user": "Gebruiker herstellen", "restored_asset": "Item hersteld", "resume": "Hervatten", @@ -2063,9 +2067,9 @@ "settings_saved": "Instellingen opgeslagen", "setup_pin_code": "Stel een pincode in", "share": "Delen", - "share_action_prompt": "{count} item(s) gedeeld", + "share_action_prompt": "{count, plural, one {# item} other {# items}} gedeeld", "share_add_photos": "Foto's toevoegen", - "share_assets_selected": "{count} item(s) geselecteerd", + "share_assets_selected": "{count, plural, one {# item} other {# items}} geselecteerd", "share_dialog_preparing": "Voorbereiden...", "share_link": "Link delen", "shared": "Gedeeld", @@ -2173,7 +2177,7 @@ "sort_title": "Titel", "source": "Bron", "stack": "Stapel", - "stack_action_prompt": "{count} item(s) gestapeld", + "stack_action_prompt": "{count} items gestapeld", "stack_duplicates": "Stapel duplicaten", "stack_select_one_photo": "Selecteer ÊÊn primaire foto voor de stapel", "stack_selected_photos": "Geselecteerde foto's stapelen", @@ -2213,6 +2217,7 @@ "tag": "Tag", "tag_assets": "Items taggen", "tag_created": "Tag aangemaakt: {tag}", + "tag_face": "Gezicht labelen", "tag_feature_description": "Bladeren door foto's en video's gegroepeerd op tags", "tag_not_found_question": "Kun je een tag niet vinden? Maak een nieuwe tag.", "tag_people": "Mensen taggen", @@ -2259,7 +2264,7 @@ "total": "Totaal", "total_usage": "Totaal gebruik", "trash": "Prullenbak", - "trash_action_prompt": "{count} item(s) verplaatst naar de prullenbak", + "trash_action_prompt": "{count, plural, one {# item} other {# items}} verplaatst naar de prullenbak", "trash_all": "Verplaats alle naar prullenbak", "trash_count": "{count, number} naar prullenbak", "trash_delete_asset": "Items naar prullenbak verplaatsen of verwijderen", @@ -2309,7 +2314,7 @@ "unselect_all_duplicates": "Deselecteer alle duplicaten", "unselect_all_in": "Deselecteer alles in {group}", "unstack": "Ontstapelen", - "unstack_action_prompt": "{count} item(s) ontstapeld", + "unstack_action_prompt": "{count} items ontstapeld", "unstacked_assets_count": "{count, plural, one {# item} other {# items}} ontstapeld", "unsupported_field_type": "Veldtype niet ondersteund", "unsupported_file_type": "Bestand {file} kan niet worden geÃŧpload omdat het bestandstype {type} niet wordt ondersteund.", @@ -2394,6 +2399,7 @@ "viewer_remove_from_stack": "Verwijder van stapel", "viewer_stack_use_as_main_asset": "Zet bovenaan de stapel", "viewer_unstack": "Ontstapel", + "visibility": "Zichtbaarheid", "visibility_changed": "Zichtbaarheid gewijzigd voor {count, plural, one {# persoon} other {# mensen}}", "visual": "Visueel", "visual_builder": "Visuele bouwer", diff --git a/i18n/nn.json b/i18n/nn.json index cbf81e4807..7a0471c574 100644 --- a/i18n/nn.json +++ b/i18n/nn.json @@ -37,8 +37,10 @@ "add_to_album_bottom_sheet_some_local_assets": "Somme lokale eigedelar kunne ikkje leggjast til i album", "add_to_albums": "Legg til i album", "add_to_albums_count": "Legg til i album ({count})", + "add_to_bottom_bar": "Legg til i", "add_to_shared_album": "Legg til i delt album", "add_url": "Legg til URL", + "add_workflow_step": "Legg til steg i arbeidsflyt", "added_to_archive": "Lagt til i arkiv", "added_to_favorites": "Lagt til i favorittar", "added_to_favorites_count": "La til {count, number} i favorittar", @@ -71,6 +73,7 @@ "confirm_reprocess_all_faces": "Er du sikker pÃĨ at du vil behandle alle ansikt pÃĨ nytt? Det vil Ã˛g fjerne namngjevne personar.", "confirm_user_password_reset": "Er du sikker at du vil tilbakestille passordet til {user}?", "confirm_user_pin_code_reset": "Er du sikker pÃĨ at du vil tilbakestille {user} sin PIN-kode?", + "copy_config_to_clipboard_description": "Kopier systemkonfigurasjonen som eit JSON-objekt til utklippstavla", "create_job": "Lag jobb", "cron_expression": "Cron uttrykk", "cron_expression_description": "Set inn skanningsintervall med cron-formatet. For meir informasjon sjÃĨ t.d. Crontab Guru", @@ -78,6 +81,7 @@ "disable_login": "Deaktiver innlogging", "duplicate_detection_job_description": "Kjør maskinlÃĻring pÃĨ filer for ÃĨ oppdage liknande bilete. Krev bruk av Smart Search", "exclusion_pattern_description": "Utelatingsmønster let deg utelate filer og mapper nÃĨr du skannar biblioteket ditt. Det er nyttig om du har mapper som inneheld filer du ikkje ynskjer ÃĨ importere, til dømes RAW-filer.", + "export_config_as_json_description": "Last ned nÃĨverande systemkonfigurasjon som ei JSON-fil", "face_detection": "Ansiktssøk", "face_detection_description": "Finn ansikt i bilete ved hjelp av maskinlÃĻring. For videoar vert berre miniatyrbilete bruka. \"Alle\" søkjer (opp att) gjennom alle bilete. \"Tilbakestill\" fjernar all gjeldande ansiktsdata. \"Manglande\" legg filer som ikkje vert behandla til i køa for ansiktssøk. Oppdaga ansikt vert lagt i køa for ansiktsattkjenning, og kopla til eksisterande eller nye personar.", "facial_recognition_job_description": "Koplar attkjende ansikt til personar. Det skjer fyrst nÃĨr anskiktssøkjet er ferdig. \"Tilbakestill\" fjernar alle koplingar til personar, og tilbakestiller ansiktsgrupper. \"Manglande\" legg ansikt som ikkje er oppkopla til i køa.", @@ -105,6 +109,7 @@ "image_thumbnail_description": "Lite miniatyrbilete med fjerna metadata, brukt nÃĨr ein ser pÃĨ grupper av bilete som hovudtidslinja", "image_thumbnail_quality_description": "Kvalitet pÃĨ miniatyrbilete frÃĨ 1-100. Høgare er betre, men gjev større filstorleik, og kan senkje appresposen.", "image_thumbnail_title": "Innstillingar for miniatyrbilete", + "import_config_from_json_description": "Importer systemkonfigurasjon ved ÃĨ laste opp ei JSON konfigurasjonsfil", "job_concurrency": "{job} samstundes utføring", "job_created": "Jobb laga", "job_not_concurrency_safe": "Kan ikke trygt utføre jobben samstundes.", @@ -112,22 +117,30 @@ "job_settings_description": "Handsam samstundes utføring av jobber", "jobs_delayed": "{jobCount, plural, other {# forsinka}}", "jobs_failed": "{jobCount, plural, other {# mislykkast}}", + "jobs_over_time": "Jobbar over tid", "library_created": "Opprett bibliotek: {library}", "library_deleted": "Bibliotek sletta", + "library_details": "Bibliotekdetaljar", + "library_folder_description": "Vel ei mappe ÃĨ importere. Denne mappa, inkludert undermappar, vil bli skanna for biletar og videoar.", + "library_remove_exclusion_pattern_prompt": "Er du sikker pÃĨ at du vil fjerne dette unntaksmønsteret?", "library_scanning": "Regelbunden skanning", "library_scanning_description": "Sett opp regelbunden skanning av biblioteket", "library_scanning_enable_description": "Aktiver regelbunden skanning av biblioteket", "library_settings": "Eksternt Bibliotek", "library_settings_description": "Handsam eksterne biblioteksinnstillingar", "library_tasks_description": "Utfør bibliotekstoppgÃĨver", + "library_updated": "Oppdatert bibliotek", "library_watching_enable_description": "Sjekk eksterne bibliotek for forandringar", "library_watching_settings": "BiblioteksovervÃĨking (EKSPERIMENTELL)", "library_watching_settings_description": "Sjekk automatisk for forandringar", "logging_enable_description": "Aktiver loggføring", "logging_level_description": "NÃĨr aktivert, kva loggnivÃĨ ÃĨ bruke.", "logging_settings": "Logging", + "machine_learning_availability_checks": "Tilgjengelegheitssjekkar", "machine_learning_availability_checks_description": "Automatiser oppdaging og prioritet av tilgjengelege maskinlÃĻrings-serverar", + "machine_learning_availability_checks_enabled": "SlÃĨ pÃĨ tilgjengelegheitssjekkar", "machine_learning_availability_checks_interval": "Sjekk intervall", + "machine_learning_availability_checks_timeout": "Tidsavbrot pÃĨ forespørsel", "machine_learning_availability_checks_timeout_description": "Utløpstid i millisekund for tilgjengelegheitssjekk", "machine_learning_clip_model": "CLIP modell", "machine_learning_clip_model_description": "Namnet pÃĨ ein CLIP modell finst her. Merk at du mÃĨ køyre 'Smart Søk'-jobben pÃĨ nytt for alle bilete etter du har forandra modell.", @@ -151,6 +164,11 @@ "machine_learning_min_detection_score_description": "Minimum tillitspoeng for at eit ansikt skal bli oppdaga, pÃĨ ein skala frÃĨ 0 til 1. LÃĨgare verdiar vil oppdage fleire ansikt, men kan føre til feilaktige treff.", "machine_learning_min_recognized_faces": "Minimum gjenkjende ansikt", "machine_learning_min_recognized_faces_description": "Minste tal pÃĨ gjenkjende fjes for ÃĨ opprette ein person. Aukar ein dette, vert ansiktsgjenkjenninga meir presis, pÃĨ bekostning av auka sjanse for at ansikt ikkje vert tileigna ein person.", + "machine_learning_ocr": "OCR", + "machine_learning_ocr_description": "Bruk maskinlÃĻring for ÃĨ gjenkjenne tekst i bilete", + "machine_learning_ocr_enabled": "SlÃĨ pÃĨ OCR", + "machine_learning_ocr_max_resolution": "Maksimal oppløysing", + "machine_learning_ocr_model": "OCR-modell", "machine_learning_settings": "Innstillingar for maskinlÃĻring", "machine_learning_settings_description": "Administrer maskinlÃĻringsfunksjonar og innstillingar", "machine_learning_smart_search": "Smart Søk", diff --git a/i18n/package.json b/i18n/package.json index a5d4a47d46..2b9548ed8b 100644 --- a/i18n/package.json +++ b/i18n/package.json @@ -1,6 +1,6 @@ { "name": "immich-i18n", - "version": "2.6.0", + "version": "2.7.5", "private": true, "scripts": { "format": "prettier --cache --check .", diff --git a/i18n/pl.json b/i18n/pl.json index d123d17077..98cd5296bc 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -91,7 +91,7 @@ "failed_job_command": "Polecenie {command} nie powiodło się dla zadania: {job}", "force_delete_user_warning": "UWAGA: UÅŧytkownik i wszystkie zasoby uÅŧytkownika zostaną natychmiast trwale usunięte. Nie moÅŧna tego cofnąć, a plikÃŗw nie będzie moÅŧna przywrÃŗcić.", "image_format": "Format", - "image_format_description": "UÅŧycie formatu WebP skutkuje utworzeniem plikÃŗw o rozmiarze mniejszym niÅŧ w przypadku JPEG ale jego kodowanie trwa dłuÅŧej.", + "image_format_description": "Format WebP generuje mniejsze pliki niÅŧ JPEG, ale ich kodowanie trwa dłuÅŧej.", "image_fullsize_description": "Pełnowymiarowy obraz z usuniętymi metadanymi, uÅŧywany przy powiększeniu", "image_fullsize_enabled": "Włącz generowanie obrazÃŗw o pełnym wymiarze", "image_fullsize_enabled_description": "Generuje pełnowymiarowe obrazy dla formatÃŗw nieprzyjaznych stronom internetowym. Gdy opcja „Preferuj osadzony podgląd” jest włączona, osadzone podglądy są uÅŧywane bezpośrednio bez konwersji. Nie wpływa na formaty przyjazne stronom internetowym, takie jak JPEG.", @@ -138,7 +138,7 @@ "library_updated": "Zaktualizowana biblioteka", "library_watching_enable_description": "Przejrzyj zewnętrzne biblioteki w poszukiwaniu zmienionych plikÃŗw", "library_watching_settings": "Obserwowanie bibliotek [EKSPERYMENTALNE]", - "library_watching_settings_description": "Automatycznie obserwuj zmienione pliki", + "library_watching_settings_description": "Automatycznie poszukuj zmian w plikach", "logging_enable_description": "Uruchom zapisywanie logÃŗw", "logging_level_description": "Kiedy włączone, jakiego poziomu uÅŧyć.", "logging_settings": "Rejestrowanie logÃŗw", @@ -441,7 +441,7 @@ "user_successfully_removed": "UÅŧytkownik {email} został pomyślnie usunięty.", "users_page_description": "Strona administracyjna do zarządzania uÅŧytkownikami", "version_check_enabled_description": "Włącz sprawdzanie wersji", - "version_check_implications": "Funkcja sprawdzania wersji opiera się na okresowej komunikacji z github.com", + "version_check_implications": "Funkcja sprawdzania wersji opiera się na okresowej komunikacji z {server}", "version_check_settings": "Sprawdzenie Wersji", "version_check_settings_description": "Włącz/wyłącz powiadomienia o nowej wersji", "video_conversion_job": "Transkodowanie wideo", @@ -849,9 +849,12 @@ "create_link_to_share": "UtwÃŗrz link do udostępnienia", "create_link_to_share_description": "PozwÃŗl kaÅŧdemu z dostępem do linku zobaczyć wybrane zdjęcie/zdjęcia", "create_new": "UTWÓRZ NOWY", + "create_new_face": "UtwÃŗrz nową twarz", "create_new_person": "StwÃŗrz nową osobę", "create_new_person_hint": "Przypisz wybrane zasoby do nowej osoby", "create_new_user": "StwÃŗrz nowego uÅŧytkownika", + "create_person": "UtwÃŗrz osobę", + "create_person_subtitle": "Dodaj nazwę do wybranej twarzy aby utworzyć i oznaczyć nową osobę", "create_shared_album_page_share_add_assets": "DODAJ ZASOBY", "create_shared_album_page_share_select_photos": "Zaznacz Zdjęcia", "create_shared_link": "UtwÃŗrz link udostępniający", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "Stałe", "crop_aspect_ratio_free": "Dowolne", "crop_aspect_ratio_original": "Oryginalne", + "crop_aspect_ratio_square": "Kwadrat", "curated_object_page_title": "Rzeczy", "current_device": "Obecne urządzenie", "current_pin_code": "Aktualny kod PIN", @@ -880,7 +884,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Ciemny", - "dark_theme": "Przełącz ciemny motyw", + "dark_theme": "Przełącz na ciemny motyw", "date": "Data", "date_after": "Data po", "date_and_time": "Data i godzina", @@ -891,10 +895,8 @@ "day": "Dzień", "days": "Dni", "deduplicate_all": "Usuń duplikaty", - "deduplication_criteria_1": "Rozmiar obrazu w bajtach", - "deduplication_criteria_2": "Ilość plikÃŗw EXIF", - "deduplication_info": "Stan duplikatÃŗw", - "deduplication_info_description": "Aby zakwalifikować elementy jako duplikaty do masowego usunięcia, sprawdzane jest:", + "default_locale": "Domyślne ustawienia regionalne", + "default_locale_description": "Formatuj daty i liczby zgodnie z ustawieniami regionalnymi przeglądarki", "delete": "Usuń", "delete_action_confirmation_message": "Jesteś pewien, Åŧe chcesz usunąć ten zasÃŗb? Ta czynność przeniesie zasÃŗb do kosza na serwerze i wyświetli komunikat z pytaniem, czy chcesz go usunąć lokalnie", "delete_action_prompt": "{count} usuniętych", @@ -970,7 +972,7 @@ "downloading_media": "Pobieranie multimediÃŗw", "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", + "duplicates_description": "Rozstrzygnij kaÅŧdą grupę, określając, ktÃŗre zasoby są duplikatami, jeÅŧeli są duplikatami.", "duration": "Czas trwania", "edit": "Edytuj", "edit_album": "Edytuj album", @@ -1007,6 +1009,8 @@ "editor_edits_applied_success": "Zmiany zostały pomyślnie zastosowane", "editor_flip_horizontal": "OdwrÃŗÄ‡ poziomo", "editor_flip_vertical": "OdwrÃŗÄ‡ pionowo", + "editor_handle_corner": "{corner, select, top_left {GÃŗrny lewy} top_right {GÃŗrny prawy} bottom_left {Dolny lewy} bottom_right {Dolny prawy} other {Jakiś}} uchwyt naroÅŧny", + "editor_handle_edge": "{edge, select, top {GÃŗrny} bottom {Dolny} left {Lewy} right {Prawy} other {Jakiś}} uchwyt krawędziowy", "editor_orientation": "Orientacja", "editor_reset_all_changes": "Zresetuj zmiany", "editor_rotate_left": "ObrÃŗÄ‡ o 90° przeciwnie do ruchu wskazÃŗwek zegara", @@ -1385,9 +1389,11 @@ "library_page_sort_title": "Tytuł albumu", "licenses": "Licencje", "light": "Jasny", + "light_theme": "Przełącz na jasny motyw", "like": "Polub", "like_deleted": "Polubienie usunięte", "link_motion_video": "Podłącz ruchome wideo", + "link_to_docs": "Więcej informacji znajdziesz w dokumentacji.", "link_to_oauth": "Połącz z OAuth", "linked_oauth_account": "Połączone konto OAuth", "list": "Lista", @@ -1567,7 +1573,7 @@ "network_requirements_updated": "Zmieniono wymagania sieciowe, resetowanie kolejki kopii zapasowych", "networking_settings": "Sieć", "networking_subtitle": "Zarządzaj ustawieniami punktu końcowego serwera", - "never": "nigdy", + "never": "Nigdy", "new_album": "Nowy album", "new_api_key": "Nowy Klucz API", "new_date_range": "Nowy zakres dat", @@ -2211,18 +2217,19 @@ "tag": "Etykieta", "tag_assets": "Ustaw etykiety zasobÃŗw", "tag_created": "Stworzono etykietę: {tag}", + "tag_face": "Oznacz twarz", "tag_feature_description": "Przeglądanie zdjęć i filmÃŗw pogrupowanych według logicznych etykiet wskazujących temat", "tag_not_found_question": "Nie moÅŧesz znaleÅēć etykiety? UtwÃŗrz ją tutaj", "tag_people": "Dodaj etykiety osÃŗb", "tag_updated": "Uaktualniono etykietę: {tag}", "tagged_assets": "Przypisano etykietę {count, plural, one {# zasobowi} other {# zasobom}}", "tags": "Etykiety", - "tap_to_run_job": "Uruchom zadanie", + "tap_to_run_job": "Naciśnij, Åŧeby uruchomić zadanie", "template": "Szablon", "text_recognition": "Rozpoznawanie tekstu", "theme": "Motyw", "theme_selection": "WybÃŗr motywu", - "theme_selection_description": "Automatycznie zmień motyw na jasny lub ciemny zaleÅŧnie od ustawień przeglądarki", + "theme_selection_description": "Automatycznie zmień motyw na jasny lub ciemny zaleÅŧnie od ustawień systemu", "theme_setting_asset_list_storage_indicator_title": "PokaÅŧ wskaÅēnik przechowywania na kafelkach zasobÃŗw", "theme_setting_asset_list_tiles_per_row_title": "Liczba zasobÃŗw w wierszu ({count})", "theme_setting_colorful_interface_subtitle": "Zastosuj kolor podstawowy do powierzchni tła.", @@ -2392,6 +2399,7 @@ "viewer_remove_from_stack": "Usuń ze stosu", "viewer_stack_use_as_main_asset": "UÅŧyj jako gÅ‚Ãŗwnego zasobu", "viewer_unstack": "Rozdziel stos", + "visibility": "Widoczność", "visibility_changed": "Zmieniono widoczność dla {count, plural, one {# osoby} other {# osÃŗb}}", "visual": "Wizualny", "visual_builder": "Edytor wizualny", diff --git a/i18n/pt.json b/i18n/pt.json index e4822f12ff..7511ed58a5 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -441,7 +441,7 @@ "user_successfully_removed": "O utilizador {email} foi removido com sucesso.", "users_page_description": "PÃĄgina de administador de utilizadores", "version_check_enabled_description": "Ativa verificaÃ§ÃŖo de novas versÃĩes", - "version_check_implications": "A funcionalidade de verificaÃ§ÃŖo da versÃŖo necessita de comunicaÃ§ÃŖo periÃŗdica com o github.com", + "version_check_implications": "A funcionalidade de verificaÃ§ÃŖo da versÃŖo necessita de comunicaÃ§ÃŖo periÃŗdica com o {server}", "version_check_settings": "VerificaÃ§ÃŖo de versÃŖo", "version_check_settings_description": "Ativar/desativar a notificaÃ§ÃŖo de nova versÃŖo", "video_conversion_job": "Transcodificar vídeos", @@ -849,9 +849,12 @@ "create_link_to_share": "Criar link para partilhar", "create_link_to_share_description": "Permitir a visualizaÃ§ÃŖo desta(s) imagem(s) a qualquer pessoa com o link", "create_new": "CRIAR NOVO", + "create_new_face": "Criar novo rosto", "create_new_person": "Criar nova pessoa", "create_new_person_hint": "Associe os ficheiros a uma nova pessoa", "create_new_user": "Criar novo utilizador", + "create_person": "Criar pessoa", + "create_person_subtitle": "Adicione um nome ao rosto selecionado para criar e etiquetar a nova pessoa", "create_shared_album_page_share_add_assets": "ADICIONAR FICHEIROS", "create_shared_album_page_share_select_photos": "Selecionar Fotos", "create_shared_link": "Criar link partilhado", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "Fixo", "crop_aspect_ratio_free": "Livre", "crop_aspect_ratio_original": "Original", + "crop_aspect_ratio_square": "Quadrado", "curated_object_page_title": "Objetos", "current_device": "Dispositivo atual", "current_pin_code": "CÃŗdigo PIN atual", @@ -880,7 +884,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Escuro", - "dark_theme": "Alternar tema escuro", + "dark_theme": "Alterar para o tema escuro", "date": "Data", "date_after": "Data apÃŗs", "date_and_time": "Data e Hora", @@ -891,10 +895,8 @@ "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", - "deduplication_info": "InformaçÃĩes sobre remoÃ§ÃŖo de duplicados", - "deduplication_info_description": "Para selecionar automaticamente itens e remover duplicados em massa, iremos ver o seguinte:", + "default_locale": "LocalizaÃ§ÃŖo PadrÃŖo", + "default_locale_description": "Formatar datas e nÃēmeros baseados na definiÃ§ÃŖo de localizaÃ§ÃŖo do navegador", "delete": "Eliminar", "delete_action_confirmation_message": "Tem a certeza de que quer eliminar este ficheiro? EstÃĄ aÃ§ÃŖo irÃĄ mover o ficheiro para a reciclagem do servidor e perguntar se quer apagÃĄ-lo localmente", "delete_action_prompt": "{count} eliminados", @@ -970,7 +972,7 @@ "downloading_media": "A descarregar ficheiro", "drop_files_to_upload": "Solte os ficheiros em qualquer lugar para os enviar", "duplicates": "Itens duplicados", - "duplicates_description": "Marque cada grupo indicando quais ficheiros, se algum, sÃŖo duplicados", + "duplicates_description": "Marca cada grupo ao indicar quais ficheiros, se algum, sÃŖo duplicados.", "duration": "DuraÃ§ÃŖo", "edit": "Editar", "edit_album": "Editar ÃĄlbum", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "Título do ÃĄlbum", "licenses": "Licenças", "light": "Claro", + "light_theme": "Alterar para o tema claro", "like": "Gosto", "like_deleted": "Gosto removido", "link_motion_video": "Relacionar video animado", + "link_to_docs": "Para mais informaçÃĩes, veja a documentaÃ§ÃŖo.", "link_to_oauth": "Link do OAuth", "linked_oauth_account": "Conta OAuth Associada", "list": "Lista", @@ -1651,6 +1655,7 @@ "only_favorites": "Apenas favoritos", "open": "Abrir", "open_calendar": "Abrir calendÃĄrio", + "open_in_browser": "Abrir no navegador", "open_in_map_view": "Abrir na visualizaÃ§ÃŖo de mapa", "open_in_openstreetmap": "Abrir no OpenStreetMap", "open_the_search_filters": "Abrir os filtros de pesquisa", @@ -2212,6 +2217,7 @@ "tag": "Etiqueta", "tag_assets": "Etiquetar ficheiros", "tag_created": "Criada a etiqueta {tag}", + "tag_face": "Etiquetar rosto", "tag_feature_description": "A mostrar fotos e videos agrupados por tÃŗpicos lÃŗgicos de etiquetas", "tag_not_found_question": "NÃŖo consegue encontrar a etiqueta? Crie uma nova etiqueta.", "tag_people": "Etiquetar Pessoas", @@ -2393,6 +2399,7 @@ "viewer_remove_from_stack": "Remover da pilha", "viewer_stack_use_as_main_asset": "Usar como foto principal", "viewer_unstack": "Desempilhar", + "visibility": "Visibilidade", "visibility_changed": "Visibilidade alterada para {count, plural, one {# pessoa} other {# pessoas}}", "visual": "Visual", "visual_builder": "Construtor visual", diff --git a/i18n/pt_BR.json b/i18n/pt_BR.json index b35fa25b4b..20d376289f 100644 --- a/i18n/pt_BR.json +++ b/i18n/pt_BR.json @@ -441,7 +441,7 @@ "user_successfully_removed": "UsuÃĄrio {email} foi removido com sucesso.", "users_page_description": "PÃĄgina de usuÃĄrios Admin", "version_check_enabled_description": "Ativa a verificaÃ§ÃŖo de versÃŖo", - "version_check_implications": "A verificaÃ§ÃŖo de versÃŖo depende de uma comunicaÃ§ÃŖo periÃŗdica com github.com", + "version_check_implications": "A verificaÃ§ÃŖo de versÃŖo depende de uma comunicaÃ§ÃŖo periÃŗdica com {server}", "version_check_settings": "VerificaÃ§ÃŖo de versÃŖo", "version_check_settings_description": "Ativar/desativar a notificaÃ§ÃŖo de nova versÃŖo", "video_conversion_job": "Transcodificar vídeos", @@ -866,6 +866,7 @@ "crop_aspect_ratio_fixed": "Fixo", "crop_aspect_ratio_free": "Livre", "crop_aspect_ratio_original": "Original", + "crop_aspect_ratio_square": "Quadrado", "curated_object_page_title": "Objetos", "current_device": "Dispositivo atual", "current_pin_code": "CÃŗdigo PIN atual", @@ -891,10 +892,7 @@ "day": "Dia", "days": "Dias", "deduplicate_all": "Limpar todas Duplicidades", - "deduplication_criteria_1": "Tamanho do arquivo em bytes", - "deduplication_criteria_2": "Quantidade de dados EXIF", - "deduplication_info": "InformaçÃĩes", - "deduplication_info_description": "Ao selecionar os arquivos que serÃŖo marcados para remoÃ§ÃŖo por duplicidade, serÃĄ considerado os parÃĸmetros:", + "default_locale": "Local padrÃŖo", "delete": "Excluir", "delete_action_confirmation_message": "Tem certeza? O arquivo serÃĄ enviado para a lixeira do servidor, depois vocÃĒ poderÃĄ confirmar se deseja tambÊm deletar do seu dispositivo local", "delete_action_prompt": "{count} deletados", @@ -1387,9 +1385,11 @@ "library_page_sort_title": "Título do ÃĄlbum", "licenses": "Licenças", "light": "Claro", + "light_theme": "Mudar para tema claro", "like": "Curtir", "like_deleted": "Curtida excluída", "link_motion_video": "Relacionar video animado", + "link_to_docs": "Para mais informaçÃĩes, veja", "link_to_oauth": "Link do OAuth", "linked_oauth_account": "Conta OAuth Vinculada", "list": "Lista", @@ -2394,6 +2394,7 @@ "viewer_remove_from_stack": "Remover do grupo", "viewer_stack_use_as_main_asset": "Usar como foto principal", "viewer_unstack": "Desagrupar", + "visibility": "Visibilidade", "visibility_changed": "A visibilidade de {count, plural, one {# pessoa foi alterada} other {# pessoas foram alteradas}}", "visual": "Visual", "visual_builder": "Construtor visual", diff --git a/i18n/ro.json b/i18n/ro.json index 9e097b4d20..a4f7e94eaa 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -441,7 +441,7 @@ "user_successfully_removed": "Utilizatorul {email} a fost șters cu succes.", "users_page_description": "Pagina utilizatorilor administratori", "version_check_enabled_description": "Activează verificarea versiunii", - "version_check_implications": "Funcția de verificare a versiunii se bazează pe comunicarea periodică cu github.com", + "version_check_implications": "Funcția de verificare a versiunii se bazează pe comunicarea periodică cu {server}", "version_check_settings": "Verificare versiune", "version_check_settings_description": "ActiveazĮŽ/dezactiveazĮŽ notificarea unei noi versiuni", "video_conversion_job": "Transcodați videoclipuri", @@ -866,6 +866,7 @@ "crop_aspect_ratio_fixed": "Reparat", "crop_aspect_ratio_free": "Liber", "crop_aspect_ratio_original": "Original", + "crop_aspect_ratio_square": "Pătrat", "curated_object_page_title": "Obiecte", "current_device": "Dispozitiv curent", "current_pin_code": "Codul PIN actual", @@ -880,7 +881,7 @@ "daily_title_text_date": "E, LLL zz", "daily_title_text_date_year": "E, LLL zz, aaaa", "dark": "Întunecat", - "dark_theme": "Comută tema ÃŽntunecată", + "dark_theme": "Comută la tema ÃŽntunecată", "date": "Dată", "date_after": "După data", "date_and_time": "Dată și oră", @@ -891,10 +892,8 @@ "day": "Zi", "days": "Zile", "deduplicate_all": "Deduplicați Toate", - "deduplication_criteria_1": "Marimea imagini ÃŽn octeți", - "deduplication_criteria_2": "Numărul de date EXIF", - "deduplication_info": "Informați despre deduplicare", - "deduplication_info_description": "Ca să preselecționăm activele și să scoatem duplicatele ÃŽn vrac , ne uităm la:", + "default_locale": "Localizare implicită", + "default_locale_description": "Formatează datele și numerele ÃŽn funcție de localizarea browser-ului", "delete": "Ștergere", "delete_action_confirmation_message": "Sigur vrei să ștergi acest element? Această acțiune va muta elementul ÃŽn coșul de gunoi al serverului și te va ÃŽntreba dacă vrei să-l ștergi local", "delete_action_prompt": "{count} șterse", @@ -970,7 +969,7 @@ "downloading_media": "Se descarcă fișierele media", "drop_files_to_upload": "Trageți fișierele aici pentru a le ÃŽncărca", "duplicates": "Duplicate", - "duplicates_description": "Rezolvați fiecare grup indicÃĸnd care sunt duplicate, dacă există", + "duplicates_description": "Rezolvați fiecare grup indicÃĸnd care sunt duplicate, dacă există.", "duration": "Durată", "edit": "Editare", "edit_album": "Editare album", diff --git a/i18n/ru.json b/i18n/ru.json index d1958d76e6..799861ebc2 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -441,7 +441,7 @@ "user_successfully_removed": "ПоĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģҌ {email} ҃ҁĐŋĐĩ҈ĐŊĐž ŅƒĐ´Đ°ĐģĐĩĐŊ.", "users_page_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅĐŧи ŅĐ¸ŅŅ‚ĐĩĐŧŅ‹", "version_check_enabled_description": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ĐŋŅ€ĐžĐ˛ĐĩŅ€Đē҃ ĐŊаĐģĐ¸Ņ‡Đ¸Ņ ĐŊĐžĐ˛Ņ‹Ņ… вĐĩŅ€ŅĐ¸Đš", - "version_check_implications": "Đ¤ŅƒĐŊĐēŅ†Đ¸Ņ ĐŋŅ€ĐžĐ˛ĐĩŅ€Đēи вĐĩŅ€ŅĐ¸Đ¸ ĐŋĐĩŅ€Đ¸ĐžĐ´Đ¸Ņ‡ĐĩҁĐēи ĐžĐąŅ€Đ°Ņ‰Đ°ĐĩŅ‚ŅŅ Đē ŅĐ°ĐšŅ‚Ņƒ github.com", + "version_check_implications": "Đ¤ŅƒĐŊĐēŅ†Đ¸Ņ ĐŋŅ€ĐžĐ˛ĐĩŅ€Đēи вĐĩŅ€ŅĐ¸Đ¸ ĐŋĐĩŅ€Đ¸ĐžĐ´Đ¸Ņ‡ĐĩҁĐēи ĐžĐąŅ€Đ°Ņ‰Đ°ĐĩŅ‚ŅŅ Đē ŅĐ°ĐšŅ‚Ņƒ {server}", "version_check_settings": "ĐŸŅ€ĐžĐ˛ĐĩŅ€Đēа вĐĩŅ€ŅĐ¸Đ¸", "version_check_settings_description": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ/ĐžŅ‚ĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊиĐĩ Đž ĐŊОвОК вĐĩŅ€ŅĐ¸Đ¸", "video_conversion_job": "ПĐĩŅ€ĐĩĐēĐžĐ´Đ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ видĐĩĐž", @@ -849,9 +849,12 @@ "create_link_to_share": "ĐĄĐžĐˇĐ´Đ°Ņ‚ŅŒ ҁҁҋĐģĐē҃ ĐžĐąŅ‰ĐĩĐŗĐž Đ´ĐžŅŅ‚ŅƒĐŋа", "create_link_to_share_description": "Đ Đ°ĐˇŅ€ĐĩŅˆĐ¸Ņ‚ŅŒ Đ˛ŅĐĩĐŧ, ҃ ĐēĐžĐŗĐž ĐĩŅŅ‚ŅŒ ҁҁҋĐģĐēа, ĐŋŅ€ĐžŅĐŧĐ°Ņ‚Ņ€Đ¸Đ˛Đ°Ņ‚ŅŒ Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐŊŅ‹Đĩ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đ¸", "create_new": "СОЗДАĐĸĐŦ НОВĐĢЙ", + "create_new_face": "ĐĄĐžĐˇĐ´Đ°Ņ‚ŅŒ ĐŊОвОĐĩ ĐģĐ¸Ņ†Đž", "create_new_person": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ ĐŊĐžĐ˛ĐžĐŗĐž ҇ĐĩĐģОвĐĩĐēа", "create_new_person_hint": "НазĐŊĐ°Ņ‡Đ¸Ņ‚ŅŒ Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐŊŅ‹Đĩ ĐžĐąŅŠĐĩĐē҂ҋ ĐŊа ĐŊĐžĐ˛ĐžĐŗĐž ҇ĐĩĐģОвĐĩĐēа", "create_new_user": "ĐĄĐžĐˇĐ´Đ°Ņ‚ŅŒ ĐŊĐžĐ˛ĐžĐŗĐž ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ", + "create_person": "ĐĄĐžĐˇĐ´Đ°Ņ‚ŅŒ ҇ĐĩĐģОвĐĩĐēа", + "create_person_subtitle": "ĐŖĐēаĐļĐ¸Ņ‚Đĩ иĐŧŅ Đ´ĐģŅ ŅĐžĐˇĐ´Đ°ĐŊĐ¸Ņ ĐŊĐžĐ˛ĐžĐŗĐž ҇ĐĩĐģОвĐĩĐēа", "create_shared_album_page_share_add_assets": "ДОБАВИĐĸĐŦ ОБĐĒЕКĐĸĐĢ", "create_shared_album_page_share_select_photos": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đ¸", "create_shared_link": "ĐĄĐžĐˇĐ´Đ°Ņ‚ŅŒ ĐžĐąŅ‰ŅƒŅŽ ҁҁҋĐģĐē҃", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "ФиĐēŅĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐŊŅ‹Đš", "crop_aspect_ratio_free": "ХвОйОдĐŊĐž", "crop_aspect_ratio_original": "ĐžŅ€Đ¸ĐŗĐ¸ĐŊаĐģ", + "crop_aspect_ratio_square": "ĐšĐ˛Đ°Đ´Ņ€Đ°Ņ‚", "curated_object_page_title": "ĐŸŅ€ĐĩĐ´ĐŧĐĩ҂ҋ", "current_device": "ĐĸĐĩĐēŅƒŅ‰ĐĩĐĩ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đž", "current_pin_code": "ĐĸĐĩĐēŅƒŅ‰Đ¸Đš PIN-ĐēОд", @@ -880,7 +884,7 @@ "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "ĐĸŅ‘ĐŧĐŊĐ°Ņ", - "dark_theme": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ/Đ˛Ņ‹ĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ҂ґĐŧĐŊŅƒŅŽ Ņ‚ĐĩĐŧ҃", + "dark_theme": "ПĐĩŅ€ĐĩĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒŅŅ ĐŊа ҂ґĐŧĐŊŅƒŅŽ Ņ‚ĐĩĐŧ҃", "date": "Đ”Đ°Ņ‚Đ°", "date_after": "Đ”Đ°Ņ‚Đ° ĐŋĐžŅĐģĐĩ", "date_and_time": "Đ”Đ°Ņ‚Đ° и Đ˛Ņ€ĐĩĐŧŅ", @@ -891,10 +895,8 @@ "day": "ДĐĩĐŊҌ", "days": "ДĐŊи", "deduplicate_all": "ĐŖĐąŅ€Đ°Ņ‚ŅŒ Đ˛ŅĐĩ Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Ņ‹", - "deduplication_criteria_1": "РаСĐŧĐĩŅ€ Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ в ĐąĐ°ĐšŅ‚Đ°Ņ…", - "deduplication_criteria_2": "КоĐģĐ¸Ņ‡ĐĩŅŅ‚Đ˛Đž EXIF даĐŊĐŊҋ҅", - "deduplication_info": "ИĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Đž Đ´ĐĩĐ´ŅƒĐŋĐģиĐēĐ°Ņ†Đ¸Đ¸", - "deduplication_info_description": "ДĐģŅ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐĩҁĐēĐžĐŗĐž Đ˛Ņ‹ĐąĐžŅ€Đ° ĐģŅƒŅ‡ŅˆĐ¸Ņ… ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ ҁҀĐĩди Đ´ŅƒĐąĐģиĐēĐ°Ņ‚ĐžĐ˛ аĐŊаĐģĐ¸ĐˇĐ¸Ņ€ŅƒĐĩŅ‚ŅŅ ҁĐģĐĩĐ´ŅƒŅŽŅ‰Đ°Ņ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ:", + "default_locale": "ЛоĐēаĐģҌ ĐŋĐž ҃ĐŧĐžĐģŅ‡Đ°ĐŊĐ¸ŅŽ", + "default_locale_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚Đ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ Đ´Đ°Ņ‚ и Ņ‡Đ¸ŅĐĩĐģ в ŅĐžĐžŅ‚Đ˛ĐĩŅ‚ŅŅ‚Đ˛Đ¸Đ¸ ҁ ŅĐˇŅ‹ĐēĐžĐ˛Ņ‹Đŧи ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēаĐŧи Đ˛Đ°ŅˆĐĩĐŗĐž ĐąŅ€Đ°ŅƒĐˇĐĩŅ€Đ°", "delete": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ", "delete_action_confirmation_message": "Đ’Ņ‹ Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ‚ĐĩĐģҌĐŊĐž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ŅŅ‚ĐžŅ‚ ĐžĐąŅŠĐĩĐēŅ‚? Đ­Ņ‚Đž Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Đĩ ĐŋĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ ĐžĐąŅŠĐĩĐēŅ‚ в ĐēĐžŅ€ĐˇĐ¸ĐŊ҃ ҁĐĩŅ€Đ˛ĐĩŅ€Đ° и ĐŋĐžĐŋŅ€ĐžĐąŅƒĐĩŅ‚ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ĐĩĐŗĐž ĐģĐžĐēаĐģҌĐŊĐž.", "delete_action_prompt": "ĐžĐąŅŠĐĩĐē҂ҋ ŅƒĐ´Đ°ĐģĐĩĐŊŅ‹ ({count} ŅˆŅ‚.)", @@ -970,7 +972,7 @@ "downloading_media": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа ĐŧĐĩдиа", "drop_files_to_upload": "ПĐĩŅ€ĐĩĐŊĐĩŅĐ¸Ņ‚Đĩ Ņ„Đ°ĐšĐģŅ‹ в ĐģŅŽĐąĐžĐĩ ĐŧĐĩŅŅ‚Đž Đ´ĐģŅ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēи", "duplicates": "Đ”ŅƒĐąĐģиĐēĐ°Ņ‚Ņ‹", - "duplicates_description": "ĐŸŅ€ĐžŅĐŧĐžŅ‚Ņ€Đ¸Ņ‚Đĩ ĐŊаКдĐĩĐŊĐŊŅ‹Đĩ Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Ņ‹ и в ĐēаĐļдОК ĐŗŅ€ŅƒĐŋĐŋĐĩ ҃ĐēаĐļĐ¸Ņ‚Đĩ, ĐēаĐēиĐĩ ĐžĐąŅŠĐĩĐē҂ҋ ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ, а ĐēаĐēиĐĩ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ", + "duplicates_description": "ĐŸŅ€ĐžŅĐŧĐžŅ‚Ņ€Đ¸Ņ‚Đĩ ĐŊаКдĐĩĐŊĐŊŅ‹Đĩ Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Ņ‹ и в ĐēаĐļдОК ĐŗŅ€ŅƒĐŋĐŋĐĩ ҃ĐēаĐļĐ¸Ņ‚Đĩ, ĐēаĐēиĐĩ ĐžĐąŅŠĐĩĐē҂ҋ ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ, а ĐēаĐēиĐĩ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ.", "duration": "ĐŸŅ€ĐžĐ´ĐžĐģĐļĐ¸Ņ‚ĐĩĐģҌĐŊĐžŅŅ‚ŅŒ", "edit": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ", "edit_album": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧ", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "НазваĐŊиĐĩ аĐģŅŒĐąĐžĐŧа", "licenses": "Đ›Đ¸Ņ†ĐĩĐŊСии", "light": "ХвĐĩŅ‚ĐģĐ°Ņ", + "light_theme": "ПĐĩŅ€ĐĩĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒŅŅ ĐŊа ŅĐ˛ĐĩŅ‚ĐģŅƒŅŽ Ņ‚ĐĩĐŧ҃", "like": "ĐŅ€Đ°Đ˛Đ¸Ņ‚ŅŅ", "like_deleted": "ЛайĐē ŅƒĐ´Đ°ĐģĐĩĐŊ", "link_motion_video": "ĐĄŅŅ‹ĐģĐēа ĐŊа двиĐļŅƒŅ‰ĐĩĐĩŅŅ видĐĩĐž", + "link_to_docs": "ДоĐŋĐžĐģĐŊĐ¸Ņ‚ĐĩĐģҌĐŊĐ°Ņ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Đ´ĐžŅŅ‚ŅƒĐŋĐŊа в Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Đ¸Đ¸.", "link_to_oauth": "ĐŸŅ€Đ¸ŅĐžĐĩдиĐŊĐĩĐŊиĐĩ Đē OAuth", "linked_oauth_account": "ĐŸŅ€Đ¸ŅĐžĐĩдиĐŊŅ‘ĐŊĐŊŅ‹Đš аĐēĐēĐ°ŅƒĐŊŅ‚ OAuth", "list": "ĐĄĐŋĐ¸ŅĐžĐē", @@ -1915,7 +1919,7 @@ "saved_settings": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи ŅĐžŅ…Ņ€Đ°ĐŊĐĩĐŊŅ‹", "say_something": "НаĐŋĐ¸ŅˆĐ¸Ņ‚Đĩ Ņ‡Ņ‚Đž-ĐŊĐ¸ĐąŅƒĐ´ŅŒ", "scaffold_body_error_occurred": "ВозĐŊиĐēĐģа ĐžŅˆĐ¸ĐąĐēа", - "scaffold_body_error_unrecoverable": "ĐŸŅ€ĐžĐ¸ĐˇĐžŅˆĐģа ĐŊĐĩŅƒŅŅ‚Ņ€Đ°ĐŊиĐŧĐ°Ņ ĐžŅˆĐ¸ĐąĐēа. ПоĐļаĐģŅƒĐšŅŅ‚Đ°, ŅĐžĐžĐąŅ‰Đ¸Ņ‚Đĩ Ой ĐžŅˆĐ¸ĐąĐēĐĩ в Discord иĐģи ĐŊа GitHub, Ņ‡Ņ‚ĐžĐąŅ‹ Ņ€Đ°ĐˇŅ€Đ°ĐąĐžŅ‚Ņ‡Đ¸Đēи ĐŧĐžĐŗĐģи ĐŋĐžĐŧĐžŅ‡ŅŒ. Đ•ŅĐģи ŅĐžĐ˛ĐĩŅ‚ŅƒŅŽŅ‚, Đ˛Ņ‹ ĐŧĐžĐļĐĩŅ‚Đĩ ĐŋĐžĐģĐŊĐžŅŅ‚ŅŒŅŽ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ даĐŊĐŊŅ‹Đĩ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊĐ¸Ņ.", + "scaffold_body_error_unrecoverable": "ĐŸŅ€ĐžĐ¸ĐˇĐžŅˆĐģа ĐŊĐĩŅƒŅŅ‚Ņ€Đ°ĐŊиĐŧĐ°Ņ ĐžŅˆĐ¸ĐąĐēа. ПоĐļаĐģŅƒĐšŅŅ‚Đ°, ŅĐžĐžĐąŅ‰Đ¸Ņ‚Đĩ Ой ŅŅ‚ĐžĐš ĐžŅˆĐ¸ĐąĐēĐĩ Ņ€Đ°ĐˇŅ€Đ°ĐąĐžŅ‚Ņ‡Đ¸ĐēаĐŧ в Discord иĐģи ĐŊа GitHub. В ĐēĐ°Ņ‡ĐĩŅŅ‚Đ˛Đĩ Ņ€Đĩ҈ĐĩĐŊĐ¸Ņ ĐŧĐžĐļĐŊĐž ĐŋĐžĐŋŅ€ĐžĐąĐžĐ˛Đ°Ņ‚ŅŒ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ даĐŊĐŊŅ‹Đĩ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊĐ¸Ņ.", "scan": "ĐŸĐžĐ¸ŅĐē", "scan_all_libraries": "ĐĄĐēаĐŊĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ Đ˛ŅĐĩ йийĐģĐ¸ĐžŅ‚ĐĩĐēи", "scan_library": "ĐĄĐēаĐŊĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ", @@ -2213,6 +2217,7 @@ "tag": "ĐĸĐĩĐŗ", "tag_assets": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ Ņ‚ĐĩĐŗĐ¸", "tag_created": "ĐĸĐĩĐŗ {tag} ŅĐžĐˇĐ´Đ°ĐŊ", + "tag_face": "ĐžŅ‚ĐŧĐĩŅ‚Đ¸Ņ‚ŅŒ ҇ĐĩĐģОвĐĩĐēа", "tag_feature_description": "ĐŸŅ€ĐžŅĐŧĐžŅ‚Ņ€ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đš и видĐĩĐž, ŅĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐŊҋ҅ ĐŋĐž Ņ‚ĐĩĐŗĐ°Đŧ", "tag_not_found_question": "НĐĩ ŅƒĐ´Đ°ĐĩŅ‚ŅŅ ĐŊĐ°ĐšŅ‚Đ¸ Ņ‚ĐĩĐŗ? ĐĄĐžĐˇĐ´Đ°ĐšŅ‚Đĩ ĐŊĐžĐ˛Ņ‹Đš Ņ‚ĐĩĐŗ.", "tag_people": "ĐžŅ‚ĐŧĐĩŅ‚Đ¸Ņ‚ŅŒ ҇ĐĩĐģОвĐĩĐēа", @@ -2394,6 +2399,7 @@ "viewer_remove_from_stack": "ĐŖĐąŅ€Đ°Ņ‚ŅŒ иС ĐŗŅ€ŅƒĐŋĐŋŅ‹", "viewer_stack_use_as_main_asset": "Đ˜ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ŅŒ в ĐēĐ°Ņ‡ĐĩŅŅ‚Đ˛Đĩ ĐžŅĐŊОвĐŊĐžĐŗĐž ĐžĐąŅŠĐĩĐēŅ‚Đ°", "viewer_unstack": "Đ Đ°ĐˇĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ", + "visibility": "ВидиĐŧĐžŅŅ‚ŅŒ", "visibility_changed": "ИСĐŧĐĩĐŊĐĩĐŊа видиĐŧĐžŅŅ‚ŅŒ ҃ {count, plural, one {# ҇ĐĩĐģОвĐĩĐēа} other {# ҇ĐĩĐģОвĐĩĐē}}", "visual": "Đ’Đ¸ĐˇŅƒĐ°ĐģҌĐŊŅ‹Đš", "visual_builder": "Đ’Đ¸ĐˇŅƒĐ°ĐģҌĐŊŅ‹Đš ĐēĐžĐŊŅŅ‚Ņ€ŅƒĐēŅ‚ĐžŅ€", diff --git a/i18n/sk.json b/i18n/sk.json index 6235815c2b..d18c2fc23b 100644 --- a/i18n/sk.json +++ b/i18n/sk.json @@ -441,7 +441,7 @@ "user_successfully_removed": "PouŞívateÄž {email} bol ÃēspeÅĄne odstrÃĄnenÃŊ.", "users_page_description": "StrÃĄnka pouŞívateÄžov pre sprÃĄvcu", "version_check_enabled_description": "PovoliÅĨ kontrolu verzie", - "version_check_implications": "Funkcia kontroly verzie sa spolieha na pravidelnÃē komunikÃĄciu s github.com", + "version_check_implications": "Funkcia kontroly verzie sa spolieha na pravidelnÃē komunikÃĄciu s {server}", "version_check_settings": "Kontrola verzie", "version_check_settings_description": "PovoliÅĨ/zakÃĄzaÅĨ upozornenia na novÃē verziu", "video_conversion_job": "PrekÃŗdovaÅĨ videÃĄ", @@ -849,9 +849,12 @@ "create_link_to_share": "VytvoriÅĨ odkaz na zdieÄžanie", "create_link_to_share_description": "UmoÅžniÅĨ kaÅždÊmu, kto mÃĄ odkaz, zobraziÅĨ vybranÊ fotografie", "create_new": "VYTVORIŤ NOVÉ", + "create_new_face": "VytvoriÅĨ novÃē tvÃĄr", "create_new_person": "VytvoriÅĨ novÃē osobu", "create_new_person_hint": "PriradiÅĨ vybranÊ poloÅžky novej osobe", "create_new_user": "Vytvorenie novÊho pouŞívateÄža", + "create_person": "VytvoriÅĨ osobu", + "create_person_subtitle": "Pridajte meno k vybranej tvÃĄri, aby ste vytvorili a označili novÃē osobu", "create_shared_album_page_share_add_assets": "PRIDAŤ POLOÅŊKY", "create_shared_album_page_share_select_photos": "VybraÅĨ fotografie", "create_shared_link": "VytvoriÅĨ zdieÄžanÃŊ odkaz", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "PevnÃŊ pomer", "crop_aspect_ratio_free": "VoÄžnÃŊ", "crop_aspect_ratio_original": "OriginÃĄlny", + "crop_aspect_ratio_square": "Å tvorec", "curated_object_page_title": "Veci", "current_device": "SÃēčasnÊ zariadenie", "current_pin_code": "AktuÃĄlny PIN kÃŗd", @@ -880,7 +884,7 @@ "daily_title_text_date": "EEEE, d. MMMM", "daily_title_text_date_year": "EEEE, d. MMMM y", "dark": "TmavÃĄ", - "dark_theme": "PrepnÃēÅĨ tmavÃē tÊmu", + "dark_theme": "PrepnÃēÅĨ na tmavÃē tÊmu", "date": "DÃĄtum", "date_after": "DÃĄtum po", "date_and_time": "DÃĄtum a Čas", @@ -891,10 +895,8 @@ "day": "Deň", "days": "Dní", "deduplicate_all": "DeduplikovaÅĨ vÅĄetko", - "deduplication_criteria_1": "VeÄžkosÅĨ obrÃĄzku v bajtoch", - "deduplication_criteria_2": "Počet EXIF Ãēdajov", - "deduplication_info": "Info o deduplikÃĄcii", - "deduplication_info_description": "Na automatickÃŊ predvÃŊber poloÅžiek a hromadnÊ odstrÃĄnenie duplicít, sa pozerÃĄme do:", + "default_locale": "PredvolenÃŊ jazyk", + "default_locale_description": "FormÃĄtovaÅĨ dÃĄtumy a čísla podÄža jazyka vÃĄÅĄho prehliadača", "delete": "VymazaÅĨ", "delete_action_confirmation_message": "Naozaj chcete tÃēto poloÅžku odstrÃĄniÅĨ? TÃĄto akcia presunie poloÅžku do koÅĄa na serveri a zobrazí sa otÃĄzka, či ju chcete odstrÃĄniÅĨ aj lokÃĄlne", "delete_action_prompt": "{count} vymazanÃŊch", @@ -970,7 +972,7 @@ "downloading_media": "SÅĨahovanie mÊdií", "drop_files_to_upload": "Umiestnite sÃēbory kamkoÄžvek na nahratie", "duplicates": "DuplikÃĄty", - "duplicates_description": "VysporiadaÅĨ sa s kaÅždou skupinou tak, Åže sa duplicitnÊ označia ako duplicitnÊ", + "duplicates_description": "VyrieÅĄiÅĨ jednotlivÊ skupiny tak, Åže sa označia tie, ktorÊ z nich sÃē duplicitnÊ, ak nejakÊ sÃē.", "duration": "Trvanie", "edit": "UpraviÅĨ", "edit_album": "UpraviÅĨ album", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "PodÄža nÃĄzvu albumu", "licenses": "Licencie", "light": "SvetlÃĄ", + "light_theme": "PrepnÃēÅĨ na svetlÃē tÊmu", "like": "PÃĄÄi sa mi", "like_deleted": "Like odstrÃĄnenÃŊ", "link_motion_video": "PripojiÅĨ pohyblivÊ video", + "link_to_docs": "ĎalÅĄie informÃĄcie nÃĄjdete v dokumentÃĄcii.", "link_to_oauth": "PrepojiÅĨ s OAuth", "linked_oauth_account": "PripojenÃŊ OAuth Ãēčet", "list": "Zoznam", @@ -2213,6 +2217,7 @@ "tag": "Å títok", "tag_assets": "PridaÅĨ ÅĄtítky", "tag_created": "VytvorenÃŊ ÅĄtítok: {tag}", + "tag_face": "OznačiÅĨ tvÃĄr", "tag_feature_description": "Prehliadanie fotiek a videÃĄ zoskupenÃŊch podÄža tematickÃŊch ÅĄtítkov", "tag_not_found_question": "Neviete nÃĄjsÅĨ ÅĄtítok? Vytvorte novÃŊ ÅĄtítok.", "tag_people": "OznačiÅĨ Äžudí", @@ -2394,6 +2399,7 @@ "viewer_remove_from_stack": "OdstrÃĄniÅĨ zo zoskupenia", "viewer_stack_use_as_main_asset": "PouÅžiÅĨ ako hlavnÃē fotku", "viewer_unstack": "ZruÅĄiÅĨ zoskupenie", + "visibility": "ViditeÄžnosÅĨ", "visibility_changed": "ViditeÄžnosÅĨ zmenenÃĄ pre {count, plural, one {# osobu} few {# osoby} other {# osôb}}", "visual": "VizuÃĄlny", "visual_builder": "VizuÃĄlny nÃĄstroj na tvorbu", diff --git a/i18n/sl.json b/i18n/sl.json index ce24a71fd3..fa6e04201e 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -441,7 +441,7 @@ "user_successfully_removed": "Uporabnik {email} je bil uspeÅĄno odstranjen.", "users_page_description": "Stran skrbniÅĄkih uporabnikov", "version_check_enabled_description": "Omogoči preverjanje različice", - "version_check_implications": "Funkcija preverjanja različic se opira na občasno komunikacijo z github.com", + "version_check_implications": "Funkcija preverjanja različic se opira na občasno komunikacijo z {server}", "version_check_settings": "Preverjanje različice", "version_check_settings_description": "Omogoči/onemogoči obvestilo o novi različici", "video_conversion_job": "Prekodiranje videoposnetkov", @@ -849,9 +849,12 @@ "create_link_to_share": "Ustvari povezavo za skupno rabo", "create_link_to_share_description": "Omogoči vsem s povezavo ogled izbranih fotografij", "create_new": "USTVARI NOVEGA", + "create_new_face": "Ustvari nov obraz", "create_new_person": "Ustvari novo osebo", "create_new_person_hint": "Dodeli izbrana sredstva novi osebi", "create_new_user": "Ustvari novega uporabnika", + "create_person": "Ustvari osebo", + "create_person_subtitle": "Dodajte ime izbranemu obrazu, da ustvarite in označite novo osebo", "create_shared_album_page_share_add_assets": "DODAJ SREDSTVA", "create_shared_album_page_share_select_photos": "Izberi fotografije", "create_shared_link": "Ustvari deljeno povezavo", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "Fiksno", "crop_aspect_ratio_free": "Poljubno", "crop_aspect_ratio_original": "Izvirno", + "crop_aspect_ratio_square": "Kvadrat", "curated_object_page_title": "Stvari", "current_device": "Trenutna naprava", "current_pin_code": "Trenutna PIN koda", @@ -880,7 +884,7 @@ "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "Temno", - "dark_theme": "Preklopi temno temo", + "dark_theme": "Preklopi na temno temo", "date": "Datum", "date_after": "Datum po", "date_and_time": "Datum in ura", @@ -891,10 +895,8 @@ "day": "Dan", "days": "Dnevi", "deduplicate_all": "Odstrani vse podvojene", - "deduplication_criteria_1": "Velikost slike v bajtih", - "deduplication_criteria_2": "Å tevilo podatkov EXIF", - "deduplication_info": "Informacije o zaznavanju dvojnikov", - "deduplication_info_description": "Za samodejno vnaprejÅĄnjo izbiro sredstev in mnoÅžično odstranjevanje dvojnikov si ogledamo:", + "default_locale": "Privzete jezikovne nastavitve", + "default_locale_description": "Oblikujte datume in ÅĄtevilke glede na jezikovne nastavitve brskalnika", "delete": "IzbriÅĄi", "delete_action_confirmation_message": "Ali ste prepričani, da Åželite izbrisati to sredstvo? S tem dejanjem boste sredstvo premaknili v koÅĄ na streÅžniku in vas pozvali, ali ga Åželite izbrisati lokalno", "delete_action_prompt": "izbrisano {count}", @@ -970,7 +972,7 @@ "downloading_media": "PrenaÅĄanje medijev", "drop_files_to_upload": "Spustite datoteke kamor koli, da jih naloÅžite", "duplicates": "Dvojniki", - "duplicates_description": "RazreÅĄite vsako skupino tako, da navedete, kateri so dvojniki, če obstajajo", + "duplicates_description": "Vsako skupino razreÅĄite tako, da navedete, kateri so, če sploh, dvojniki.", "duration": "Trajanje", "edit": "Uredi", "edit_album": "Uredi album", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "Naslov albuma", "licenses": "Licence", "light": "Svetlo", + "light_theme": "Preklopi na svetlo temo", "like": "VÅĄeč mi je", "like_deleted": "VÅĄeček izbrisan", "link_motion_video": "Povezava videa gibanja", + "link_to_docs": "Za več informacij glejte dokumentacijo.", "link_to_oauth": "Povezava do OAuth", "linked_oauth_account": "Povezan račun OAuth", "list": "Seznam", @@ -2213,6 +2217,7 @@ "tag": "Oznaka", "tag_assets": "Označi sredstva", "tag_created": "Ustvarjena oznaka: {tag}", + "tag_face": "Označi obraz", "tag_feature_description": "Brskanje po fotografijah in videoposnetkih, razvrÅĄÄenih po temah logičnih oznak", "tag_not_found_question": "Ne najdete oznake? Ustvarite novo oznako.", "tag_people": "Označi osebe", @@ -2394,6 +2399,7 @@ "viewer_remove_from_stack": "Odstrani iz sklada", "viewer_stack_use_as_main_asset": "Uporabi kot glavno sredstvo", "viewer_unstack": "Razkladi", + "visibility": "Vidljivost", "visibility_changed": "Vidnost spremenjena za {count, plural, one {# osebo} two {# osebi} few {# osebe} other {# oseb}}", "visual": "Vizualno", "visual_builder": "Vizualni graditelj", diff --git a/i18n/sr_Cyrl.json b/i18n/sr_Cyrl.json index 00fc4b3087..1472b111a4 100644 --- a/i18n/sr_Cyrl.json +++ b/i18n/sr_Cyrl.json @@ -370,7 +370,7 @@ "user_settings": "ПодĐĩŅˆĐ°Đ˛Đ°ŅšĐ° ĐēĐžŅ€Đ¸ŅĐŊиĐēа", "user_settings_description": "ĐŖĐŋŅ€Đ°Đ˛Ņ™Đ°Ņ˜Ņ‚Đĩ ĐēĐžŅ€Đ¸ŅĐŊĐ¸Ņ‡ĐēиĐŧ ĐŋОдĐĩŅˆĐ°Đ˛Đ°ŅšĐ¸Đŧа", "version_check_enabled_description": "ОĐŧĐžĐŗŅƒŅ›Đ¸ ĐŋŅ€ĐžĐ˛ĐĩŅ€Ņƒ ĐŊĐžĐ˛Đ¸Ņ… Đ¸ĐˇĐ´Đ°ŅšĐ°", - "version_check_implications": "Đ¤ŅƒĐŊĐēŅ†Đ¸Ņ˜Đ° ĐŋŅ€ĐžĐ˛ĐĩŅ€Đĩ вĐĩŅ€ĐˇĐ¸Ņ˜Đĩ ҁĐĩ ĐžŅĐģĐ°ŅšĐ° ĐŊа ĐŋĐĩŅ€Đ¸ĐžĐ´Đ¸Ņ‡ĐŊ҃ ĐēĐžĐŧ҃ĐŊиĐēĐ°Ņ†Đ¸Ņ˜Ņƒ ŅĐ° ĐŗĐ¸Ņ‚Ņ…ŅƒĐą.Ņ†ĐžĐŧ", + "version_check_implications": "Đ¤ŅƒĐŊĐēŅ†Đ¸Ņ˜Đ° ĐŋŅ€ĐžĐ˛ĐĩŅ€Đĩ вĐĩŅ€ĐˇĐ¸Ņ˜Đĩ ҁĐĩ ĐžŅĐģĐ°ŅšĐ° ĐŊа ĐŋĐĩŅ€Đ¸ĐžĐ´Đ¸Ņ‡ĐŊ҃ ĐēĐžĐŧ҃ĐŊиĐēĐ°Ņ†Đ¸Ņ˜Ņƒ ŅĐ° {server}", "version_check_settings": "ĐŸŅ€ĐžĐ˛ĐĩŅ€Đ° вĐĩŅ€ĐˇĐ¸Ņ˜Đĩ", "version_check_settings_description": "ОĐŧĐžĐŗŅƒŅ›Đ¸/ĐžĐŊĐĩĐŧĐžĐŗŅƒŅ›Đ¸ ОйавĐĩŅˆŅ‚ĐĩҚĐĩ Đž ĐŊĐžĐ˛ĐžŅ˜ вĐĩŅ€ĐˇĐ¸Ņ˜Đ¸", "video_conversion_job": "ĐĸŅ€Đ°ĐŊҁĐēĐžĐ´Đ¸Ņ€Đ°ŅšĐĩ видĐĩĐž СаĐŋĐ¸ŅĐ°", @@ -733,10 +733,6 @@ "day": "ДаĐŊ", "days": "ДаĐŊи", "deduplicate_all": "ДĐĩ-Đ´ŅƒĐŋĐģĐ¸Ņ†Đ¸Ņ€Đ°Ņ˜ ŅĐ˛Đĩ", - "deduplication_criteria_1": "ВĐĩĐģĐ¸Ņ‡Đ¸ĐŊа ҁĐģиĐēĐĩ ҃ ĐąĐ°Ņ˜Ņ‚ĐžĐ˛Đ¸Đŧа", - "deduplication_criteria_2": "Đ‘Ņ€ĐžŅ˜ EXIF ĐŋĐžĐ´Đ°Ņ‚Đ°Đēа", - "deduplication_info": "ИĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ˜Đĩ Đž Đ´ĐĩĐ´ŅƒĐŋĐģиĐēĐ°Ņ†Đ¸Ņ˜Đ¸", - "deduplication_info_description": "Да ĐąĐ¸ŅĐŧĐž Đ°ŅƒŅ‚ĐžĐŧĐ°Ņ‚ŅĐēи ҃ĐŊаĐŋŅ€ĐĩĐ´ ĐžĐ´Đ°ĐąŅ€Đ°Đģи Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēĐĩ и ҃ĐēĐģĐžĐŊиĐģи Đ´ŅƒĐŋĐģиĐēĐ°Ņ‚Đĩ ĐŗŅ€ŅƒĐŋĐŊĐž, ĐŗĐģĐĩдаĐŧĐž:", "delete": "ĐžĐąŅ€Đ¸ŅˆĐ¸", "delete_album": "ĐžĐąŅ€Đ¸ŅˆĐ¸ аĐģĐąŅƒĐŧ", "delete_api_key_prompt": "Да Đģи ҁ҂Đĩ ŅĐ¸ĐŗŅƒŅ€ĐŊи да ĐļĐĩĐģĐ¸Ņ‚Đĩ да Đ¸ĐˇĐąŅ€Đ¸ŅˆĐĩŅ‚Đĩ ĐžĐ˛Đ°Ņ˜ АПИ ĐēŅ™ŅƒŅ‡ (ĐēĐĩy)?", diff --git a/i18n/sr_Latn.json b/i18n/sr_Latn.json index d09e1a1abf..b7f71ba4b8 100644 --- a/i18n/sr_Latn.json +++ b/i18n/sr_Latn.json @@ -441,7 +441,7 @@ "user_successfully_removed": "Korisnik {email} je uspeÅĄno uklonjen.", "users_page_description": "Stranica administratorskih korisnika", "version_check_enabled_description": "Omogucˁite proveru novih izdanja", - "version_check_implications": "Funkcija provere verzije se oslanja na periodičnu komunikaciju sa github.com", + "version_check_implications": "Funkcija provere verzije se oslanja na periodičnu komunikaciju sa {server}", "version_check_settings": "Provera verzije", "version_check_settings_description": "Omogucˁite/onemogucˁite obaveÅĄtenje o novoj verziji", "video_conversion_job": "Transkodiranje video zapisa", @@ -879,10 +879,6 @@ "day": "Dan", "days": "Dani", "deduplicate_all": "De-dupliciraj sve", - "deduplication_criteria_1": "Veličina slike u bajtovima", - "deduplication_criteria_2": "Broj EXIF podataka", - "deduplication_info": "Informacije o deduplikaciji", - "deduplication_info_description": "Da bismo automatski unapred odabrali datoteke i uklonili duplikate grupno, gledamo:", "delete": "ObriÅĄi", "delete_action_confirmation_message": "Da li sigurno ÅželiÅĄ da obriÅĄeÅĄ ovu stvar? Ova akcija će pomeriti stvar u serversku kantu i ponuditi da li ÅželiÅĄ da je obriÅĄeÅĄ i lokalno", "delete_action_prompt": "{count} obrisano", diff --git a/i18n/sv.json b/i18n/sv.json index 82c2398b02..a9c63cc836 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -425,10 +425,10 @@ "unlink_all_oauth_accounts_description": "Kom ihÃĨg att ta bort länken till alla OAuth-konton innan du migrerar till en ny leverantÃļr.", "unlink_all_oauth_accounts_prompt": "Är du säker pÃĨ att du vill ta bort länken till alla OAuth-konton? Detta ÃĨterställer OAuth-ID:t fÃļr varje användare och kan inte ÃĨngras.", "user_cleanup_job": "Användarrensning", - "user_delete_delay": "{user} 's konto och objekt kommer att schemaläggas fÃļr permanent radering om {delay, plural, one {# day} other {# days}}.", + "user_delete_delay": "{user}s konto och resurser kommer att schemaläggas fÃļr permanent radering om {delay, plural, one {# dag} other {# dagar}}.", "user_delete_delay_settings": "BorttagningsfÃļrdrÃļjning", "user_delete_delay_settings_description": "Antal dagar efter borttagning fÃļr att permanent radera en användares konto och objekt. Arbetet med borttagning av användare kÃļrs vid midnatt fÃļr att sÃļka efter användare som är redo fÃļr radering. Ändringar av denna inställning kommer att utvärderas vid nästa kÃļrning.", - "user_delete_immediately": "{user} konto och objekt kommer att stÃĨ i kÃļ fÃļr permanent radering.", + "user_delete_immediately": "{user}s konto och resurser kommer att stÃĨ i kÃļ fÃļr omedelbar permanent radering.", "user_delete_immediately_checkbox": "KÃļa användare och objekt fÃļr omedelbar radering", "user_details": "Användardetaljer", "user_management": "Användarhantering", @@ -441,7 +441,7 @@ "user_successfully_removed": "Användaren {email} har tagits bort.", "users_page_description": "AdministratÃļrsanvändare", "version_check_enabled_description": "Aktivera versionskontroll", - "version_check_implications": "Funktionen fÃļr versionskontroll är beroende av periodisk kommunikation med github.com", + "version_check_implications": "Funktionen fÃļr versionskontroll är beroende av periodisk kommunikation med {server}", "version_check_settings": "Versionskontroll", "version_check_settings_description": "Aktivera/inaktivera notis om ny version", "video_conversion_job": "Omkoda videor", @@ -558,16 +558,16 @@ "asset_action_delete_err_read_only": "Kan inte ta bort skrivskyddade objekt, hoppar Ãļver", "asset_action_share_err_offline": "Kan inte hämta offline-objekt, hoppar Ãļver", "asset_added_to_album": "Lades till i album", - "asset_adding_to_album": "Lägger till i album...â€Ļ", + "asset_adding_to_album": "Lägger till i albumâ€Ļ", "asset_created": "Objekt skapad", "asset_description_updated": "Objektbeskrivning har uppdaterats", "asset_filename_is_offline": "Objektet {filename} är offline", "asset_has_unassigned_faces": "Objektet har otilldelade ansikten", - "asset_hashing": "Hashing...â€Ļ", + "asset_hashing": "Hashningâ€Ļ", "asset_list_group_by_sub_title": "Gruppera pÃĨ", "asset_list_layout_settings_dynamic_layout_title": "Dynamisk layout", "asset_list_layout_settings_group_automatically": "Automatiskt", - "asset_list_layout_settings_group_by": "Gruppera bilder efter", + "asset_list_layout_settings_group_by": "Gruppera resurser efter", "asset_list_layout_settings_group_by_month_day": "MÃĨnad + dag", "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Layoutinställningar fÃļr bildrutnät", @@ -583,32 +583,32 @@ "asset_trashed": "Objekt kasserat", "asset_troubleshoot": "FelsÃļkning av objekt", "asset_uploaded": "Uppladdad", - "asset_uploading": "Laddar upp...â€Ļ", - "asset_viewer_settings_subtitle": "Hantera inställningar fÃļr gallerivisare", + "asset_uploading": "Laddar uppâ€Ļ", + "asset_viewer_settings_subtitle": "Hantera inställningar fÃļr gallerivisning", "asset_viewer_settings_title": "Objektvisare", "assets": "Objekt", - "assets_added_count": "La till {count, plural, one {# asset} other {# assets}}", - "assets_added_to_album_count": "Lade till {count, plural, one {# asset} other {# assets}} i albumet", - "assets_added_to_albums_count": "Lade till {assetTotal, plural, one {# asset} other {# assets}} till {albumTotal, plural, one {# album} other {# albums}}", - "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} kan inte läggas till i albumet", - "assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} kan inte läggas till i nÃĨgot av albumen", + "assets_added_count": "Lade till {count, plural, one {# resurs} other {# resurser}}", + "assets_added_to_album_count": "Lade till {count, plural, one {# resurs} other {# resurser}} i albumet", + "assets_added_to_albums_count": "Lade till {assetTotal, plural, one {# resurs} other {# resurser}} till {albumTotal, plural, one {# album} other {# album}}", + "assets_cannot_be_added_to_album_count": "{count, plural, one {Resurs} other {Resurser}} kan inte läggas till i albumet", + "assets_cannot_be_added_to_albums": "{count, plural, one {Resurs} other {Resurser}} kan inte läggas till i nÃĨgot av albumen", "assets_count": "{count, plural, one {# objekt} other {# objekt}}", - "assets_deleted_permanently": "{count} objekt har tagits bort permanent", - "assets_deleted_permanently_from_server": "{count} objekt har tagits bort permanent frÃĨn Immich-servern", - "assets_downloaded_failed": "{count, plural, one {Nedladdad # fil - {error} fil misslyckades} other {Nedladdade # filer - {error} filer misslyckades}}", - "assets_downloaded_successfully": "{count, plural, one {Nedladdade # fil framgÃĨngsrikt} other {Nedladdade # filer framgÃĨngsrikt}}", - "assets_moved_to_trash_count": "Flyttade {count, plural, one {# asset} other {# assets}} till papperskorgen", - "assets_permanently_deleted_count": "Raderad permanent {count, plural, one {# asset} other {# assets}}", - "assets_removed_count": "Tog bort {count, plural, one {# asset} other {# assets}}", - "assets_removed_permanently_from_device": "{count} objekt har raderats permanent frÃĨn din enhet", - "assets_restore_confirmation": "Är du säker pÃĨ att du vill ÃĨterställa alla dina papperskorgen? Du kan inte ÃĨngra den här ÃĨtgärden! Observera att offlineobjekt inte kan ÃĨterställas pÃĨ detta sätt.", - "assets_restored_count": "Återställd {count, plural, one {# asset} other {# assets}}", - "assets_restored_successfully": "{count} objekt har ÃĨterställts", - "assets_trashed": "{count} objekt raderade", - "assets_trashed_count": "Till Papperskorgen {count, plural, one {# asset} other {# assets}}", + "assets_deleted_permanently": "{count, plural, one {# resurs} other {# resurser}} har raderats permanent", + "assets_deleted_permanently_from_server": "{count, plural, one {# resurs} other {# resurser}} har permanent raderats frÃĨn Immich-servern", + "assets_downloaded_failed": "{count, plural, one {Nerladdning av # fil - {error} fil misslyckades} other {Nerladdning av # filer - {error} filer misslyckades}}", + "assets_downloaded_successfully": "{count, plural, one {# fil framgÃĨngsrikt nerladdad} other {# filer framgÃĨngsrikt nerladdade}}", + "assets_moved_to_trash_count": "Flyttade {count, plural, one {# resurs} other {# resurser}} till papperskorgen", + "assets_permanently_deleted_count": "{count, plural, one {# resurs} other {# resurser}} permanent raderade", + "assets_removed_count": "Tog bort {count, plural, one {# resurs} other {# resurser}}", + "assets_removed_permanently_from_device": "{count, plural, one {# resurs} other {# resurser}} har raderats permanent frÃĨn din enhet", + "assets_restore_confirmation": "Är du säker pÃĨ att du vill ÃĨterställa alla dina slängda resurser? Du kan inte ÃĨngra den här ÃĨtgärden! Observera att offlineresurser inte kan ÃĨterställas pÃĨ detta sätt.", + "assets_restored_count": "Återställde {count, plural, one {# resurs} other {# resurser}}", + "assets_restored_successfully": "{count, plural, one {# resurs} other {# resurser}} har framgÃĨngsrikt ÃĨterställts", + "assets_trashed": "{count, plural, one {# resurs} other {# resurser}} {count, plural, one {flyttad} other {flyttade}} till papperskorgen", + "assets_trashed_count": "{count, plural, one {# resurs} other {# resurser}} {count, plural, one {flyttad} other {flyttade}} till papperskorgen", "assets_trashed_from_server": "{count} objekt raderade frÃĨn Immich-servern", - "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Asset were}} är redan en del av albumet", - "assets_were_part_of_albums_count": "{count, plural, one {Asset was} other {Asset were}} redan del av albumen", + "assets_were_part_of_album_count": "{count, plural, one {Resursen} other {Resurserna}} tillhÃļr redan albumet", + "assets_were_part_of_albums_count": "{count, plural, one {Resursen} other {Resurserna}} tillhÃļr redan albumen", "authorized_devices": "Auktoriserade enheter", "automatic_endpoint_switching_subtitle": "Anslut lokalt via det angivna Wi-Fi-nätverket när det är tillgängligt och använd alternativa anslutningar pÃĨ andra platser", "automatic_endpoint_switching_title": "Automatisk URL-växling", @@ -622,14 +622,14 @@ "backup": "Säkerhetskopiera", "backup_album_selection_page_albums_device": "Album pÃĨ enhet ({count})", "backup_album_selection_page_albums_tap": "Tryck en gÃĨng fÃļr att inkludera, tryck tvÃĨ gÃĨnger fÃļr att exkludera", - "backup_album_selection_page_assets_scatter": "Objekt kan vara utspridda Ãļver flera album. DärfÃļr kan album inkluderas eller exkluderas under säkerhetskopieringsprocessen.", + "backup_album_selection_page_assets_scatter": "Resurser kan vara utspridda Ãļver flera album. DärfÃļr kan album inkluderas eller exkluderas under säkerhetskopieringsprocessen.", "backup_album_selection_page_select_albums": "Välj album", "backup_album_selection_page_selection_info": "Info om valda objekt", - "backup_album_selection_page_total_assets": "Antal unika objekt", + "backup_album_selection_page_total_assets": "Antal unika resurser", "backup_albums_sync": "Backup-albumsynkronisering", "backup_all": "Allt", - "backup_background_service_backup_failed_message": "Säkerhetskopiering av foton och videor misslyckades. FÃļrsÃļker igenâ€Ļ", - "backup_background_service_complete_notification": "Säkerhetskopiering av objekt klar", + "backup_background_service_backup_failed_message": "Säkerhetskopiering av resurser misslyckades. FÃļrsÃļker igenâ€Ļ", + "backup_background_service_complete_notification": "Säkerhetskopiering av resurser slutfÃļrt", "backup_background_service_connection_failed_message": "Anslutning till servern misslyckades. FÃļrsÃļker igenâ€Ļ", "backup_background_service_current_upload_notification": "Laddar upp {filename}", "backup_background_service_default_notification": "SÃļker efter nya objektâ€Ļ", @@ -678,7 +678,7 @@ "backup_controller_page_uploading_file_info": "Laddar upp filinformation", "backup_err_only_album": "Kan inte ta bort det enda albumet", "backup_error_sync_failed": "Synkroniseringen misslyckades. Det gÃĨr inte att bearbeta säkerhetskopian.", - "backup_info_card_assets": "objekt", + "backup_info_card_assets": "resurser", "backup_manual_cancelled": "Avbrutet", "backup_manual_in_progress": "Uppladdning pÃĨgÃĨr redan. FÃļrsÃļk igen om en liten stund", "backup_manual_success": "Klart", @@ -700,8 +700,8 @@ "build": "Bygge", "build_image": "Byggfil", "bulk_delete_duplicates_confirmation": "Är du säker pÃĨ att du vill massradera {count, plural, one {# dublettobjekt} other {# dublettobjekt}}? Detta kommer att behÃĨlla det stÃļrsta objektet i varje grupp och permanent radera alla andra dubbletter. Du kan inte ÃĨngra den här ÃĨtgärden!", - "bulk_keep_duplicates_confirmation": "Är du säker pÃĨ att du vill behÃĨlla {count, plural, one {# duplicate asset} other {# duplicate assets}}? Detta kommer att lÃļsa alla dubbletter av grupper utan att ta bort nÃĨgonting.", - "bulk_trash_duplicates_confirmation": "Är du säker pÃĨ att du vill skicka {count, plural, one {# dublettobjekt} other {# dublettobjekt}} till papperskorgen? Detta kommer att behÃĨlla det stÃļrsta objektet i varje grupp och alla andra dubbletter kasseras.", + "bulk_keep_duplicates_confirmation": "Är du säker pÃĨ att du vill behÃĨlla {count, plural, one {# dublett} other {# dubletter}}? Detta kommer att lÃļsa alla grupper av dubbletter utan att ta bort nÃĨgonting.", + "bulk_trash_duplicates_confirmation": "Är du säker pÃĨ att du vill skicka {count, plural, one {# dublett} other {# dubletter}} till papperskorgen? Detta kommer att behÃĨlla det stÃļrsta objektet i varje grupp och alla andra dubbletter kasseras.", "buy": "KÃļp Immich", "cache_settings_clear_cache_button": "Rensa cacheminnet", "cache_settings_clear_cache_button_title": "Rensar appens cacheminne. Detta kommer att avsevärt pÃĨverka appens prestanda tills cachen har byggts om.", @@ -752,22 +752,22 @@ "changed_visibility_successfully": "Synligheten har ändrats", "charging": "Laddar", "charging_requirement_mobile_backup": "Bakgrundssäkerhetskopiering kräver att enheten laddas", - "check_corrupt_asset_backup": "Kontrollera om det finns korrupta säkerhetskopior av objekt", + "check_corrupt_asset_backup": "Kontrollera om det finns korrupta resursbackuper", "check_corrupt_asset_backup_button": "Kontrollera", - "check_corrupt_asset_backup_description": "KÃļr kontrollen endast Ãļver Wi-Fi och när alla objekt har säkerhetskopierats. Det kan ta nÃĨgra minuter.", + "check_corrupt_asset_backup_description": "KÃļr kontrollen endast Ãļver Wi-Fi och när alla resurser har säkerhetskopierats. Det kan ta nÃĨgra minuter.", "check_logs": "Kontrollera loggar", "checksum": "Checksumma", "choose_matching_people_to_merge": "Välj matchande personer att slÃĨ samman", "city": "Stad", - "cleanup_confirm_description": "Immich hittade {count} material (skapade fÃļre {date} som säkerhetskopierats säkert till servern. Ta bort de lokala kopiorna frÃĨn den här enheten?", + "cleanup_confirm_description": "Immich hittade {count} resurser (skapade fÃļre {date}) som säkerhetskopierats säkert till servern. Ta bort de lokala kopiorna frÃĨn den här enheten?", "cleanup_confirm_prompt_title": "Ta bort frÃĨn den här enheten?", - "cleanup_deleted_assets": "Flyttade {count} material till enhetens papperskorg", + "cleanup_deleted_assets": "Flyttade {count, plural, one {# resurs} other {# resurser}} till enhetens papperskorg", "cleanup_deleting": "Flyttar till papperskorg...", - "cleanup_found_assets": "Hittade {count} säkerhetskopierade material", + "cleanup_found_assets": "Hittade {count} {count, plural, one {säkerhetskopierad resurs} other {säkerhetskopierade resurser}}", "cleanup_found_assets_with_size": "Hittade {count} säkerhetskopierade objekt ({size})", "cleanup_icloud_shared_albums_excluded": "iCloud delade album exkluderas frÃĨn skanningen", - "cleanup_no_assets_found": "Inga objekt hittades som matchar kriterierna ovan. FrigÃļr utrymme kan bara ta bort objekt som har säkerhetskopierats till servern", - "cleanup_preview_title": "Material att ta bort {count}", + "cleanup_no_assets_found": "Inga objekt hittades som matchar kriterierna ovan. FrigÃļr Utrymme kan bara ta bort objekt som har säkerhetskopierats till servern", + "cleanup_preview_title": "Resurser att ta bort ({count})", "cleanup_step3_description": "Skanna efter säkerhetskopierade objekt som matchar ditt datum och behÃĨll inställningarna.", "cleanup_step4_summary": "{count} objekt (skapade fÃļre {date}) att tas bort frÃĨn din lokala enhet. Foton kommer att fÃļrbli tillgängliga frÃĨn Immich-appen.", "cleanup_trash_hint": "FÃļr att helt frigÃļra lagringsutrymme, Ãļppna systemgalleriappen och tÃļm papperskorgen", @@ -807,7 +807,7 @@ "completed": "Klar", "confirm": "Bekräfta", "confirm_admin_password": "Bekräfta administratÃļrslÃļsenord", - "confirm_delete_face": "Är du säker pÃĨ att du vill ta bort {name}'s ansikte frÃĨn objektet?", + "confirm_delete_face": "Är du säker pÃĨ att du vill ta bort {name}s ansikte frÃĨn objektet?", "confirm_delete_shared_link": "Är du säker pÃĨ att du vill ta bort den här delade länken?", "confirm_keep_this_delete_others": "Alla objekt fÃļrutom den här tas bort frÃĨn hÃļgen. Är du säker pÃĨ att du vill fortsätta?", "confirm_new_pin_code": "Bekräfta ny PIN-kod", @@ -849,9 +849,12 @@ "create_link_to_share": "Skapa länk att dela", "create_link_to_share_description": "LÃĨt alla med länken se de valda fotona", "create_new": "SKAPA NY", + "create_new_face": "Skapa nytt ansikte", "create_new_person": "Skapa ny person", "create_new_person_hint": "Tilldela valda objekt till en ny person", "create_new_user": "Skapa en ny användare", + "create_person": "Skapa person", + "create_person_subtitle": "Lägg till ett namn till det valda ansiktet fÃļr att skapa och tagga den nya personen", "create_shared_album_page_share_add_assets": "LÄGG TILL OBJEKT", "create_shared_album_page_share_select_photos": "Välj bilder", "create_shared_link": "Skapa delad länk", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "Fixat", "crop_aspect_ratio_free": "Fritt", "crop_aspect_ratio_original": "Original", + "crop_aspect_ratio_square": "Kvadrat", "curated_object_page_title": "Objekt", "current_device": "Aktuell enhet", "current_pin_code": "Nuvarande PIN-kod", @@ -880,7 +884,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "MÃļrk", - "dark_theme": "Växla mÃļrkt tema", + "dark_theme": "Växla till mÃļrkt tema", "date": "Datum", "date_after": "Datum efter", "date_and_time": "Datum och Tid", @@ -891,10 +895,8 @@ "day": "Dag", "days": "Dagar", "deduplicate_all": "Deduplicera alla", - "deduplication_criteria_1": "Bildstorlek i bytes", - "deduplication_criteria_2": "Räkning av EXIF-data", - "deduplication_info": "Dedupliceringsinformation", - "deduplication_info_description": "FÃļr att automatiskt välja filer och ta bort dubletter i bulk analyserar vi:", + "default_locale": "StandardsprÃĨk", + "default_locale_description": "Formatera datum och siffror baserat pÃĨ din webbläsares sprÃĨkinställningar", "delete": "Radera", "delete_action_confirmation_message": "Är du säker pÃĨ att du vill ta bort det här objektet? Den här ÃĨtgärden flyttar objektet till serverns papperskorg och frÃĨgar om du vill ta bort den lokalt", "delete_action_prompt": "{count} raderade", @@ -923,7 +925,7 @@ "delete_tag_confirmation_prompt": "Är du säker pÃĨ att du vill ta bort {tagName}-taggen?", "delete_user": "Ta bort användare", "deleted_shared_link": "Ta bort delad länk", - "deletes_missing_assets": "Tar bort objekt som saknas frÃĨn disken", + "deletes_missing_assets": "Tar bort objekt som saknas pÃĨ disken", "description": "Beskrivning", "description_input_hint_text": "Lägg till beskrivning...", "description_input_submit_error": "Fel vid uppdatering av beskrivning, se loggen fÃļr fler detaljer", @@ -959,18 +961,18 @@ "download_original": "Ladda ner ursprunglig fil", "download_paused": "Nedladdning pausad", "download_settings": "Ladda ner", - "download_settings_description": "Hantera inställningar relaterade till nedladdning av objekt", + "download_settings_description": "Hantera inställningar relaterade till nedladdning av resurser", "download_started": "Nedladdning pÃĨbÃļrjad", "download_sucess": "Nedladdning lyckades", "download_sucess_android": "Media har laddats ner till DCIM/Immich", "download_waiting_to_retry": "Väntar pÃĨ omfÃļrsÃļk", "downloading": "Laddar ner", - "downloading_asset_filename": "Laddar ned objekt {filename}", + "downloading_asset_filename": "Laddar ner objekt {filename}", "downloading_from_icloud": "Laddar ner frÃĨn iCloud", "downloading_media": "Laddar ner media", "drop_files_to_upload": "Släpp filer var som helst fÃļr att ladda upp", "duplicates": "Dubletter", - "duplicates_description": "LÃļs varje grupp genom att ange vilka, om nÃĨgra, är dubbletter", + "duplicates_description": "LÃļs varje grupp genom att ange vilka, om nÃĨgra, är dubbletter.", "duration": "Varaktighet", "edit": "Redigera", "edit_album": "Redigera album", @@ -1007,6 +1009,8 @@ "editor_edits_applied_success": "Redigeringarna har tillämpats framgÃĨngsrikt", "editor_flip_horizontal": "Vänd horisontellt", "editor_flip_vertical": "Vänd vertikalt", + "editor_handle_corner": "{corner, select, top_left {Övre vänstra} top_right {Övre hÃļgra} bottom_left {Nedre vänstra} bottom_right {Nedre hÃļgra} other {A}} hÃļrn handtag", + "editor_handle_edge": "{edge, select, top {Övre} bottom {Nedre} left {Vänster} right {HÃļger} other {En}} hÃļrnhandtag", "editor_orientation": "Orientering", "editor_reset_all_changes": "Återställ ändringar", "editor_rotate_left": "Rotera 90° moturs", @@ -1042,8 +1046,8 @@ "cannot_navigate_previous_asset": "Det gÃĨr inte att navigera till fÃļregÃĨende objekt", "cant_apply_changes": "Det gÃĨr inte att tillämpa ändringar", "cant_change_activity": "Kan inte {enabled, select, true {avaktivera} other {aktivera}} aktivitet", - "cant_change_asset_favorite": "Det gÃĨr inte att byta favorit mot objekt", - "cant_change_metadata_assets_count": "Det gÃĨr inte att ändra metadata fÃļr {count, plural, one {# asset} other {# assets}}", + "cant_change_asset_favorite": "Det gÃĨr inte att byta favorit fÃļr objekt", + "cant_change_metadata_assets_count": "Det gÃĨr inte att ändra metadata fÃļr {count, plural, one {# resurs} other {# resurser}}", "cant_get_faces": "Kan inte fÃĨ ansikten", "cant_get_number_of_comments": "Kan inte fÃĨ antal kommentarer", "cant_search_people": "Kan inte sÃļka efter personer", @@ -1060,7 +1064,7 @@ "failed_to_create_shared_link": "Det gick inte att skapa delad länk", "failed_to_edit_shared_link": "Det gick inte att redigera delad länk", "failed_to_get_people": "Det gick inte att hämta personer", - "failed_to_keep_this_delete_others": "Misslyckades att behÃĨlla detta objekt radera Ãļvriga objekt", + "failed_to_keep_this_delete_others": "Misslyckades att behÃĨlla detta objekt och radera Ãļvriga objekt", "failed_to_load_asset": "Det gick inte att ladda objekt", "failed_to_load_assets": "Det gick inte att ladda objekten", "failed_to_load_notifications": "Misslyckades med att ladda notifikationer", @@ -1273,17 +1277,17 @@ "hide_schema": "GÃļm schema", "hide_text_recognition": "DÃļlj textigenkänning", "hide_unnamed_people": "GÃļm personer utan namn", - "home_page_add_to_album_conflicts": "Lade till {added} foton och videor i albumet {album}. {failed} foton och videor finns redan i albumet.", + "home_page_add_to_album_conflicts": "Lade till {added} resurser i albumet {album}. {failed} resurser finns redan i albumet.", "home_page_add_to_album_err_local": "Kan inte lägga till lokala objekt till album ännu, hoppar Ãļver", - "home_page_add_to_album_success": "Lade till {added} foton och videor i albumet {album}.", + "home_page_add_to_album_success": "Lade till {added} resurser i albumet {album}.", "home_page_album_err_partner": "Kan inte lägga till partner-objekt till album ännu, hoppar Ãļver", "home_page_archive_err_local": "Kan inte arkivera lokala objekt ännu, hoppar Ãļver", "home_page_archive_err_partner": "Kan inte arkivera partner-objekt, hoppar Ãļver", "home_page_building_timeline": "Bygger tidslinjen", "home_page_delete_err_partner": "Kan inte ta bort partner-objekt, hoppar Ãļver", "home_page_delete_remote_err_local": "Lokala objekt i urvalet fÃļr att ta bort frÃĨn servern, hoppar Ãļver", - "home_page_favorite_err_local": "Kan inte favorisera lokala objekt ännu, hoppar Ãļver", - "home_page_favorite_err_partner": "Kan inte favorisera partner-objekt ännu, hoppar Ãļver", + "home_page_favorite_err_local": "Kan inte favoritmarkera lokala objekt ännu, hoppar Ãļver", + "home_page_favorite_err_partner": "Kan inte favoritmarkera partner-objekt ännu, hoppar Ãļver", "home_page_first_time_notice": "Om det här är fÃļrsta gÃĨngen du använder appen, välj ett eller flera backup-album sÃĨ att tidslinjen kan fyllas med foton och videor frÃĨn albumen", "home_page_locked_error_local": "Kan inte flytta lokala resurser till lÃĨst mapp, hoppar Ãļver", "home_page_locked_error_partner": "Kan inte flytta partnerresurser till lÃĨst mapp, hoppar Ãļver", @@ -1321,7 +1325,7 @@ "in_year_selector": "In", "include_archived": "Inkludera arkiverade", "include_shared_albums": "Inkludera delade album", - "include_shared_partner_assets": "Inkludera delade partners objekt", + "include_shared_partner_assets": "Inkludera partnerdelade resurser", "individual_share": "Enskild delning", "individual_shares": "Individuella delningar", "info": "Information", @@ -1385,9 +1389,11 @@ "library_page_sort_title": "Albumtitel", "licenses": "Licenser", "light": "Ljus", + "light_theme": "Ändra till ljust tema", "like": "Gilla", "like_deleted": "Gilla borttagen", "link_motion_video": "Länka rÃļrlig video", + "link_to_docs": "FÃļr mer information, se dokumentationen.", "link_to_oauth": "Länk till OAuth", "linked_oauth_account": "Länkat OAuth konto", "list": "Lista", @@ -2211,6 +2217,7 @@ "tag": "Tagg", "tag_assets": "Tagga objekt", "tag_created": "Skapade tagg: {tag}", + "tag_face": "Tagga ansikte", "tag_feature_description": "Bläddra bland foton och videor grupperade efter logiska taggar", "tag_not_found_question": "Kan du inte hitta en tagg? Skapa en ny tagg.", "tag_people": "Tagga Personer", @@ -2392,6 +2399,7 @@ "viewer_remove_from_stack": "Ta bort frÃĨn Stapeln", "viewer_stack_use_as_main_asset": "Använd som Huvudobjekt", "viewer_unstack": "Stapla Av", + "visibility": "Synlighet", "visibility_changed": "Synlighet ändrad fÃļr {count, plural, one {# person} other {# personer}}", "visual": "Visuellt", "visual_builder": "Visuell byggare", diff --git a/i18n/ta.json b/i18n/ta.json index f33c148fd5..482be2e993 100644 --- a/i18n/ta.json +++ b/i18n/ta.json @@ -61,8 +61,8 @@ "backup_onboarding_1_description": "āŽŽā¯‡āŽ•āŽŽā¯ āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽĩā¯‡āŽąā¯ āŽ‡āŽŸāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽ¨āŽ•āŽ˛ā¯.", "backup_onboarding_2_description": "āŽĩ❆āŽĩā¯āŽĩā¯‡āŽąā¯ āŽšāŽžāŽ¤āŽŠāŽ™ā¯āŽ•āŽŗāŽŋāŽ˛ā¯ āŽ‰āŽŗā¯āŽŗ āŽ¨āŽ•āŽ˛ā¯ āŽĒāŽŋāŽ°āŽ¤āŽŋāŽ•āŽŗā¯. āŽ‡āŽ¤āŽŋāŽ˛ā¯ āŽŽā¯āŽ•ā¯āŽ•āŽŋāŽ¯ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽ…āŽ¨ā¯āŽ¤āŽ•ā¯ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽ•āŽŗāŽŋāŽŠā¯ āŽ¨āŽ•āŽ˛ā¯ āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽ†āŽ•āŽŋāŽ¯āŽĩ❈ āŽ…āŽŸāŽ™ā¯āŽ•ā¯āŽŽā¯.", "backup_onboarding_3_description": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ¤āŽ°āŽĩāŽŋāŽŠā¯ āŽŽā¯ŠāŽ¤ā¯āŽ¤ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽ•āŽŗā¯ āŽ…āŽšāŽ˛ā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽ¨āŽ•āŽ˛ā¯āŽ•āŽŗā¯ āŽ‰āŽŸā¯āŽĒāŽŸ. āŽ‡āŽ¤āŽŋāŽ˛ā¯ 1 āŽĩā¯†āŽŗāŽŋāŽĒā¯āŽĒā¯āŽą āŽ¨āŽ•āŽ˛ā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ 2 āŽšāŽžāŽ¤āŽŠāŽĒā¯ āŽĒāŽŋāŽ°āŽ¤āŽŋāŽ•āŽŗā¯ āŽ…āŽŸāŽ™ā¯āŽ•ā¯āŽŽā¯.", - "backup_onboarding_description": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ¤āŽ°āŽĩ❈ āŽĒāŽžāŽ¤ā¯āŽ•āŽžāŽĒā¯āŽĒāŽ¤āŽąā¯āŽ•āŽžāŽ• āŽ’āŽ°ā¯ 3-2-1 āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯ āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽĒāŽ°āŽŋāŽ¨ā¯āŽ¤ā¯āŽ°ā¯ˆāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽ•āŽŋāŽąāŽ¤ā¯. āŽŽā¯āŽ´ā¯āŽŽā¯ˆāŽ¯āŽžāŽŠ āŽ•āŽžāŽĒā¯āŽĒ❁ āŽĒāŽžāŽ¤ā¯āŽ•āŽžāŽĒā¯āŽĒ❁ āŽ¤ā¯€āŽ°ā¯āŽĩāŽŋāŽąā¯āŽ•āŽžāŽ•, āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽŋāŽ¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯/āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ Immich āŽ¤āŽ°āŽĩā¯āŽ¤ā¯āŽ¤āŽŗāŽ¤ā¯āŽ¤āŽŋāŽŠā¯ āŽ¨āŽ•āŽ˛ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽĩā¯ˆāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ•ā¯āŽ• āŽĩā¯‡āŽŖā¯āŽŸā¯āŽŽā¯.", - "backup_onboarding_footer": "Immich-āŽ āŽ¤āŽ°āŽĩ❁ āŽ¨āŽ•āŽ˛ā¯ āŽ•āŽžāŽĒā¯āŽĒ❁ āŽŽāŽŸā¯āŽĒā¯āŽĒāŽ¤ā¯ āŽĒāŽąā¯āŽąāŽŋāŽ¯ āŽŽā¯‡āŽ˛ā¯āŽŽā¯ āŽ¤āŽ•āŽĩāŽ˛ā¯āŽ•ā¯āŽ•ā¯, āŽ¤āŽ¯āŽĩā¯āŽšā¯†āŽ¯ā¯āŽ¤ā¯ āŽ†āŽĩāŽŖāŽ¤ā¯āŽ¤ā¯ˆ āŽĒāŽžāŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯.", + "backup_onboarding_description": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ¤āŽ°āŽĩ❈āŽĒā¯ āŽĒāŽžāŽ¤ā¯āŽ•āŽžāŽĒā¯āŽĒāŽ¤āŽąā¯āŽ•āŽžāŽ• āŽ’āŽ°ā¯ 3-2-1 āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯ āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽĒāŽ°āŽŋāŽ¨ā¯āŽ¤ā¯āŽ°ā¯ˆāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽ•āŽŋāŽąāŽ¤ā¯. āŽŽā¯āŽ´ā¯āŽŽā¯ˆāŽ¯āŽžāŽŠ āŽ•āŽžāŽĒā¯āŽĒ❁ āŽĒāŽžāŽ¤ā¯āŽ•āŽžāŽĒā¯āŽĒ❁ āŽ¤ā¯€āŽ°ā¯āŽĩāŽŋāŽąā¯āŽ•āŽžāŽ•, āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽŋāŽ¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯/āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ Immich āŽ¤āŽ°āŽĩā¯āŽ¤ā¯āŽ¤āŽŗāŽ¤ā¯āŽ¤āŽŋāŽŠā¯ āŽ¨āŽ•āŽ˛ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽĩā¯ˆāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ•ā¯āŽ• āŽĩā¯‡āŽŖā¯āŽŸā¯āŽŽā¯.", + "backup_onboarding_footer": "āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯-āŽ āŽ¤āŽ°āŽĩ❁ āŽ¨āŽ•āŽ˛ā¯ āŽ•āŽžāŽĒā¯āŽĒ❁ āŽŽāŽŸā¯āŽĒā¯āŽĒāŽ¤ā¯ āŽĒāŽąā¯āŽąāŽŋāŽ¯ āŽŽā¯‡āŽ˛ā¯āŽŽā¯ āŽ¤āŽ•āŽĩāŽ˛ā¯āŽ•ā¯āŽ•ā¯, āŽ¤āŽ¯āŽĩā¯āŽšā¯†āŽ¯ā¯āŽ¤ā¯ āŽ†āŽĩāŽŖāŽ¤ā¯āŽ¤ā¯ˆ āŽĒāŽžāŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯.", "backup_onboarding_parts_title": "3-2-1 āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋāŽ¯āŽŋāŽ˛ā¯ āŽĒāŽŋāŽŠā¯āŽĩāŽ°ā¯āŽĩāŽŠ āŽ…āŽŸāŽ™ā¯āŽ•ā¯āŽŽā¯:", "backup_onboarding_title": "āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋāŽ•āŽŗā¯", "backup_settings": "āŽ¤āŽ°āŽĩā¯āŽ¤ā¯āŽ¤āŽŗ āŽ¤āŽŋāŽŖāŽŋāŽĒā¯āŽĒ❁ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯", @@ -104,6 +104,8 @@ "image_preview_description": "āŽ…āŽ•āŽąā¯āŽąāŽĒā¯āŽĒāŽŸā¯āŽŸ āŽŽā¯†āŽŸā¯āŽŸāŽžāŽŸā¯‡āŽŸā¯āŽŸāŽžāŽĩā¯āŽŸāŽŠā¯ āŽ¨āŽŸā¯āŽ¤ā¯āŽ¤āŽ° āŽ…āŽŗāŽĩāŽŋāŽ˛āŽžāŽŠ āŽĒāŽŸāŽŽā¯, āŽ’āŽąā¯āŽąā¯ˆ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ˆāŽĒā¯ āŽĒāŽžāŽ°ā¯āŽ•ā¯āŽ•ā¯āŽŽā¯āŽĒā¯‹āŽ¤ā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽ‡āŽ¯āŽ¨ā¯āŽ¤āŽŋāŽ° āŽ•āŽąā¯āŽąāŽ˛ā¯āŽ•ā¯āŽ•āŽžāŽ•āŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĒā¯āŽĒāŽŸā¯āŽ•āŽŋāŽąāŽ¤ā¯", "image_preview_quality_description": "1-100 āŽŽā¯āŽ¤āŽ˛ā¯ āŽ¤āŽ°āŽ¤ā¯āŽ¤ā¯ˆ āŽŽā¯āŽŠā¯āŽŠā¯‹āŽŸā¯āŽŸāŽŽāŽŋāŽŸā¯āŽ™ā¯āŽ•āŽŗā¯. āŽ‰āŽ¯āŽ°ā¯āŽ¨ā¯āŽ¤āŽ¤ā¯ āŽšāŽŋāŽąāŽ¨ā¯āŽ¤āŽ¤ā¯, āŽ†āŽŠāŽžāŽ˛ā¯ āŽĒā¯†āŽ°āŽŋāŽ¯ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯āŽŸā¯ āŽŽāŽąā¯āŽŽā¯ŠāŽ´āŽŋāŽ¯ā¯ˆāŽ•ā¯ āŽ•ā¯āŽąā¯ˆāŽ•ā¯āŽ•ā¯āŽŽā¯. āŽ•ā¯āŽąā¯ˆāŽ¨ā¯āŽ¤ āŽŽāŽ¤āŽŋāŽĒā¯āŽĒ❈ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒāŽ¤ā¯ āŽ‡āŽ¯āŽ¨ā¯āŽ¤āŽŋāŽ° āŽ•āŽąā¯āŽąāŽ˛ā¯ āŽ¤āŽ°āŽ¤ā¯āŽ¤ā¯ˆ āŽĒāŽžāŽ¤āŽŋāŽ•ā¯āŽ•āŽ˛āŽžāŽŽā¯.", "image_preview_title": "āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ āŽŽā¯āŽŠā¯āŽŠā¯‹āŽŸā¯āŽŸāŽŽā¯", + "image_progressive": "āŽŽā¯āŽąā¯āŽĒā¯‹āŽ•ā¯āŽ•āŽžāŽŠāŽ¤ā¯", + "image_progressive_description": "āŽĒāŽŸāŽŋāŽĒā¯āŽĒāŽŸāŽŋāŽ¯āŽžāŽ• āŽāŽąā¯āŽąāŽĒā¯āŽĒāŽŸā¯āŽŽā¯ āŽ•āŽžāŽŸā¯āŽšāŽŋāŽ•ā¯āŽ•ā¯ JPEG āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ˆ āŽĒāŽŸāŽŋāŽĒā¯āŽĒāŽŸāŽŋāŽ¯āŽžāŽ• āŽ•ā¯āŽąāŽŋāŽ¯āŽžāŽ•ā¯āŽ•āŽŽā¯ āŽšā¯†āŽ¯ā¯āŽ¯āŽĩā¯āŽŽā¯. āŽ‡āŽ¤ā¯ WebP āŽĒāŽŸāŽ™ā¯āŽ•āŽŗāŽŋāŽ˛ā¯ āŽŽāŽ¨ā¯āŽ¤ āŽĩāŽŋāŽŗā¯ˆāŽĩā¯ˆāŽ¯ā¯āŽŽā¯ āŽāŽąā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽžāŽ¤ā¯.", "image_quality": "āŽ¤āŽ°āŽŽā¯", "image_resolution": "āŽĒāŽ•ā¯āŽ¤ā¯āŽ¤āŽ˛ā¯", "image_resolution_description": "āŽ…āŽ¤āŽŋāŽ• āŽ¤ā¯€āŽ°ā¯āŽŽāŽžāŽŠāŽ™ā¯āŽ•āŽŗā¯ āŽ…āŽ¤āŽŋāŽ• āŽĩāŽŋāŽĩāŽ°āŽ™ā¯āŽ•āŽŗā¯ˆ āŽĒāŽžāŽ¤ā¯āŽ•āŽžāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯ā¯āŽŽā¯, āŽ†āŽŠāŽžāŽ˛ā¯ āŽ•ā¯āŽąāŽŋāŽ¯āŽžāŽ•ā¯āŽ• āŽ…āŽ¤āŽŋāŽ• āŽ¨ā¯‡āŽ°āŽŽā¯ āŽŽāŽŸā¯āŽ•ā¯āŽ•ā¯āŽŽā¯, āŽĒā¯†āŽ°āŽŋāŽ¯ āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽ…āŽŗāŽĩā¯āŽ•āŽŗā¯ˆāŽ•ā¯ āŽ•ā¯ŠāŽŖā¯āŽŸāŽŋāŽ°ā¯āŽ•ā¯āŽ•āŽ˛āŽžāŽŽā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯āŽŸā¯ āŽŽāŽąā¯āŽŽā¯ŠāŽ´āŽŋāŽ¯ā¯ˆāŽ•ā¯ āŽ•ā¯āŽąā¯ˆāŽ•ā¯āŽ•āŽ˛āŽžāŽŽā¯.", @@ -189,10 +191,20 @@ "machine_learning_smart_search_enabled_description": "āŽŽā¯āŽŸāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŋāŽ°ā¯āŽ¨ā¯āŽ¤āŽžāŽ˛ā¯, āŽ¸ā¯āŽŽāŽžāŽ°ā¯āŽŸā¯ āŽ¤ā¯‡āŽŸāŽ˛ā¯āŽ•ā¯āŽ•āŽžāŽ• āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ āŽ•ā¯āŽąāŽŋāŽ¯āŽžāŽ•ā¯āŽ•āŽŽā¯ āŽšā¯†āŽ¯ā¯āŽ¯āŽĒā¯āŽĒāŽŸāŽžāŽ¤ā¯.", "machine_learning_url_description": "āŽ‡āŽ¯āŽ¨ā¯āŽ¤āŽŋāŽ° āŽ•āŽąā¯āŽąāŽ˛ā¯ āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ•āŽ¤ā¯āŽ¤āŽŋāŽŠā¯ āŽŽā¯āŽ•āŽĩāŽ°āŽŋ. āŽ’āŽŠā¯āŽąā¯āŽ•ā¯āŽ•ā¯ āŽŽā¯‡āŽąā¯āŽĒāŽŸā¯āŽŸ āŽŽā¯āŽ•āŽĩāŽ°āŽŋ āŽĩāŽ´āŽ™ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽžāŽ˛ā¯, āŽ’āŽĩā¯āŽĩā¯ŠāŽ°ā¯ āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ•āŽŽā¯āŽŽā¯ āŽ’āŽĩā¯āŽĩā¯ŠāŽŠā¯āŽąāŽžāŽ• āŽĩā¯†āŽąā¯āŽąāŽŋāŽ•āŽ°āŽŽāŽžāŽ• āŽĒāŽ¤āŽŋāŽ˛āŽŗāŽŋāŽ•ā¯āŽ•ā¯āŽŽā¯ āŽĩāŽ°ā¯ˆ, āŽŽā¯āŽ¤āŽ˛āŽŋāŽ˛ā¯ āŽ‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽ•āŽŸā¯ˆāŽšāŽŋ āŽĩāŽ°ā¯ˆ āŽŽā¯āŽ¯āŽąā¯āŽšāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯. āŽĒāŽ¤āŽŋāŽ˛āŽŗāŽŋāŽ•ā¯āŽ•āŽžāŽ¤ āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ•āŽ™ā¯āŽ•āŽŗā¯ āŽŽā¯€āŽŖā¯āŽŸā¯āŽŽā¯ āŽ†āŽŠā¯āŽ˛ā¯ˆāŽŠāŽŋāŽ˛ā¯ āŽĩāŽ°ā¯āŽŽā¯ āŽĩāŽ°ā¯ˆ āŽ¤āŽąā¯āŽ•āŽžāŽ˛āŽŋāŽ•āŽŽāŽžāŽ•āŽĒā¯ āŽĒā¯āŽąāŽ•ā¯āŽ•āŽŖāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯.", "maintenance_delete_backup": "āŽ•āŽžāŽĒā¯āŽĒā¯āŽ•ā¯āŽ•āŽŗā¯ˆ āŽ¨ā¯€āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "maintenance_delete_backup_description": "āŽ‡āŽ¨ā¯āŽ¤āŽ•ā¯ āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽŽā¯€āŽŗāŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽŽāŽ˛ā¯ āŽ¨ā¯€āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯.", + "maintenance_delete_error": "āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋāŽ¯ā¯ˆ āŽ¨ā¯€āŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ.", + "maintenance_restore_backup": "āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋāŽ¯ā¯ˆ āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆ", + "maintenance_restore_backup_description": "āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋāŽ¯āŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯ āŽ¤ā¯āŽŸā¯ˆāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸā¯ āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯. āŽ¤ā¯ŠāŽŸāŽ°ā¯āŽĩāŽ¤āŽąā¯āŽ•ā¯ āŽŽā¯āŽŠā¯ āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯.", + "maintenance_restore_backup_different_version": "āŽ‡āŽ¨ā¯āŽ¤ āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯āŽšāŽŋāŽŠā¯ āŽĩā¯‡āŽąā¯āŽĒāŽŸā¯āŽŸ āŽĒāŽ¤āŽŋāŽĒā¯āŽĒā¯ˆāŽ•ā¯ āŽ•ā¯ŠāŽŖā¯āŽŸā¯ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯!", + "maintenance_restore_backup_unknown_version": "āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯ āŽĒāŽ¤āŽŋāŽĒā¯āŽĒā¯ˆāŽ¤ā¯ āŽ¤ā¯€āŽ°ā¯āŽŽāŽžāŽŠāŽŋāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ.", + "maintenance_restore_database_backup": "āŽ¤āŽ°āŽĩā¯āŽ¤ā¯āŽ¤āŽŗ āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋāŽ¯ā¯ˆ āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "maintenance_restore_database_backup_description": "āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯ āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽ•ā¯‹āŽĒā¯āŽĒ❈āŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽŋ āŽŽā¯āŽ¨ā¯āŽ¤ā¯ˆāŽ¯ āŽ¤āŽ°āŽĩā¯āŽ¤ā¯āŽ¤āŽŗ āŽ¨āŽŋāŽ˛ā¯ˆāŽ•ā¯āŽ•ā¯ āŽ¤āŽŋāŽ°ā¯āŽŽā¯āŽĒāŽĩā¯āŽŽā¯", "maintenance_settings": "āŽĒāŽ°āŽžāŽŽāŽ°āŽŋāŽĒā¯āŽĒ❁", "maintenance_settings_description": "āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯āŽšā¯ˆ āŽĒāŽ°āŽžāŽŽāŽ°āŽŋāŽĒā¯āŽĒ❁ āŽŽā¯āŽąā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽĩā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯.", - "maintenance_start": "āŽĒāŽ°āŽžāŽŽāŽ°āŽŋāŽĒā¯āŽĒ❁ āŽĒāŽ¯āŽŠā¯āŽŽā¯āŽąā¯ˆāŽ¯ā¯ˆāŽ¤ā¯ āŽ¤ā¯ŠāŽŸāŽ™ā¯āŽ•ā¯", + "maintenance_start": "āŽĒāŽ°āŽžāŽŽāŽ°āŽŋāŽĒā¯āŽĒ❁ āŽŽā¯āŽąā¯ˆāŽ•ā¯āŽ•ā¯ āŽŽāŽžāŽąāŽĩā¯āŽŽā¯", "maintenance_start_error": "āŽĒāŽ°āŽžāŽŽāŽ°āŽŋāŽĒā¯āŽĒ❁ āŽĒāŽ¯āŽŠā¯āŽŽā¯āŽąā¯ˆāŽ¯ā¯ˆāŽ¤ā¯ āŽ¤ā¯ŠāŽŸāŽ™ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ.", + "maintenance_upload_backup": "āŽ¤āŽ°āŽĩā¯āŽ¤ā¯āŽ¤āŽŗ āŽ•āŽžāŽĒā¯āŽĒ❁ āŽ•ā¯‹āŽĒā¯āŽĒ❈ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽĩā¯āŽŽā¯", + "maintenance_upload_backup_error": "āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋāŽ¯ā¯ˆāŽĒā¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽą āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ, āŽ‡āŽ¤ā¯ .sql/.sql.gz āŽ•ā¯‹āŽĒā¯āŽĒāŽžāŽ•ā¯āŽŽāŽž?", "manage_concurrency": "āŽ’āŽ¤ā¯āŽ¤āŽŋāŽšā¯ˆāŽĩ❈ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "manage_concurrency_description": "āŽĩā¯‡āŽ˛ā¯ˆ āŽ’āŽ°ā¯āŽ™ā¯āŽ•āŽŋāŽŖā¯ˆāŽĩ❈ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ• āŽĩā¯‡āŽ˛ā¯ˆāŽ•āŽŗā¯ āŽĒāŽ•ā¯āŽ•āŽ¤ā¯āŽ¤āŽŋāŽąā¯āŽ•ā¯āŽšā¯ āŽšā¯†āŽ˛ā¯āŽ˛āŽĩā¯āŽŽā¯", "manage_log_settings": "āŽĒāŽ¤āŽŋāŽĩ❁ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -206,7 +218,7 @@ "map_reverse_geocoding": "āŽĒ❁āŽĩāŽŋ āŽ‡āŽ°ā¯āŽĒā¯āŽĒāŽŋāŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽ¤ā¯€āŽ°ā¯āŽŽāŽžāŽŠāŽŋāŽ¤ā¯āŽ¤āŽ˛ā¯", "map_reverse_geocoding_enable_description": "āŽĒ❁āŽĩāŽŋāŽ‡āŽ°ā¯āŽĒā¯āŽĒāŽŋāŽŸ āŽ¤ā¯€āŽ°ā¯āŽŽāŽžāŽŠāŽ¤ā¯āŽ¤ā¯ˆ āŽšā¯†āŽ¯āŽ˛ā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", "map_reverse_geocoding_settings": "āŽĒ❁āŽĩāŽŋāŽ‡āŽ°ā¯āŽĒā¯āŽĒāŽŋāŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽ¤ā¯€āŽ°ā¯āŽŽāŽžāŽŠāŽŋāŽ¤ā¯āŽ¤āŽ˛ā¯ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯", - "map_settings": "āŽŽā¯‡āŽĒā¯ & āŽœāŽŋāŽĒāŽŋāŽŽāŽ¸ā¯ (GPS) āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯", + "map_settings": "āŽĩāŽ°ā¯ˆāŽĒāŽŸāŽŽā¯", "map_settings_description": "āŽŽā¯‡āŽĒā¯ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "map_style_description": "style.json āŽŽā¯‡āŽĒā¯ āŽ¤ā¯€āŽŽā¯āŽ•ā¯āŽ•āŽžāŽŠ URL", "memory_cleanup_job": "āŽ¨āŽŋāŽŠā¯ˆāŽĩāŽ•āŽ¤ā¯āŽ¤ā¯ˆ āŽšā¯āŽ¤ā¯āŽ¤āŽŽā¯ āŽšā¯†āŽ¯ā¯āŽ¤āŽ˛ā¯", @@ -260,7 +272,7 @@ "oauth_auto_register": "āŽ¤āŽžāŽŠāŽŋāŽ¯āŽ™ā¯āŽ•ā¯ āŽĒāŽ¤āŽŋāŽĩ❁", "oauth_auto_register_description": "OAuth āŽ‰āŽŸāŽŠā¯ āŽ‰āŽŗā¯āŽ¨ā¯āŽ´ā¯ˆāŽ¨ā¯āŽ¤ āŽĒāŽŋāŽąāŽ•ā¯ āŽ¤āŽžāŽŠāŽžāŽ•āŽĩ❇ āŽĒā¯āŽ¤āŽŋāŽ¯ āŽĒāŽ¯āŽŠāŽ°ā¯āŽ•āŽŗā¯ˆāŽĒā¯ āŽĒāŽ¤āŽŋāŽĩā¯āŽšā¯†āŽ¯ā¯āŽ¯āŽĩā¯āŽŽā¯", "oauth_button_text": "āŽĒāŽŸā¯āŽŸāŽŠā¯ āŽ‰āŽ°ā¯ˆ", - "oauth_client_secret_description": "āŽ…āŽĩāŽšāŽŋāŽ¯āŽŽā¯, OAuth āŽĩāŽ´āŽ™ā¯āŽ•ā¯āŽ¨āŽ°āŽžāŽ˛ā¯ PKCE (āŽ•ā¯āŽąāŽŋāŽ¯ā¯€āŽŸā¯āŽŸā¯āŽĒā¯ āŽĒāŽ°āŽŋāŽŽāŽžāŽąā¯āŽąāŽ¤ā¯āŽ¤āŽŋāŽąā¯āŽ•āŽžāŽŠ āŽ†āŽ¤āŽžāŽ° āŽĩāŽŋāŽšā¯ˆ) āŽ†āŽ¤āŽ°āŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽžāŽĩāŽŋāŽŸā¯āŽŸāŽžāŽ˛ā¯", + "oauth_client_secret_description": "āŽ°āŽ•āŽšāŽŋāŽ¯ āŽĩāŽžāŽŸāŽŋāŽ•ā¯āŽ•ā¯ˆāŽ¯āŽžāŽŗāŽ°ā¯āŽ•ā¯āŽ•ā¯āŽ¤ā¯ āŽ¤ā¯‡āŽĩ❈, āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽĒā¯ŠāŽ¤ā¯ āŽ•āŽŋāŽŗā¯ˆāŽ¯āŽŖā¯āŽŸāŽŋāŽąā¯āŽ•ā¯ PKCE (āŽ•ā¯āŽąāŽŋāŽ¯ā¯€āŽŸā¯āŽŸā¯ āŽĒāŽ°āŽŋāŽŽāŽžāŽąā¯āŽąāŽ¤ā¯āŽ¤āŽŋāŽąā¯āŽ•āŽžāŽŠ āŽ†āŽ¤āŽžāŽ° āŽĩāŽŋāŽšā¯ˆ) āŽ†āŽ¤āŽ°āŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽžāŽĩāŽŋāŽŸā¯āŽŸāŽžāŽ˛ā¯.", "oauth_enable_description": "OAuth āŽŽā¯‚āŽ˛āŽŽā¯ āŽ‰āŽŗā¯āŽ¨ā¯āŽ´ā¯ˆāŽ•", "oauth_mobile_redirect_uri": "āŽŽā¯ŠāŽĒā¯ˆāŽ˛ā¯ āŽĩāŽ´āŽŋāŽŽāŽžāŽąā¯āŽąā¯ URI", "oauth_mobile_redirect_uri_override": "āŽŽā¯ŠāŽĒā¯ˆāŽ˛ā¯ āŽĩāŽ´āŽŋāŽŽāŽžāŽąā¯āŽąā¯ URI āŽŽā¯‡āŽ˛ā¯†āŽ´ā¯āŽ¤ā¯āŽ¤āŽ˛ā¯", @@ -269,7 +281,7 @@ "oauth_role_claim_description": "āŽ‡āŽ¨ā¯āŽ¤āŽ•ā¯ āŽ•ā¯‹āŽ°āŽŋāŽ•ā¯āŽ•ā¯ˆāŽ¯āŽŋāŽŠā¯ āŽ‡āŽ°ā¯āŽĒā¯āŽĒāŽŋāŽŠā¯ āŽ…āŽŸāŽŋāŽĒā¯āŽĒāŽŸā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽ¤āŽžāŽŠāŽžāŽ•āŽĩ❇ āŽ¨āŽŋāŽ°ā¯āŽĩāŽžāŽ•āŽŋ āŽ…āŽŖā¯āŽ•āŽ˛ā¯ˆ āŽĩāŽ´āŽ™ā¯āŽ•āŽĩā¯āŽŽā¯. āŽ•ā¯‹āŽ°āŽŋāŽ•ā¯āŽ•ā¯ˆāŽ¯āŽŋāŽ˛ā¯ 'āŽĒāŽ¯āŽŠāŽ°ā¯' āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ 'āŽ¨āŽŋāŽ°ā¯āŽĩāŽžāŽ•āŽŋ' āŽ‡āŽ°ā¯āŽ•ā¯āŽ•āŽ˛āŽžāŽŽā¯.", "oauth_settings": "āŽ“āŽ†āŽ¤ā¯", "oauth_settings_description": "OAuth āŽ‰āŽŗā¯āŽ¨ā¯āŽ´ā¯ˆāŽĩ❁ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", - "oauth_settings_more_details": "āŽ‡āŽ¨ā¯āŽ¤ āŽ…āŽŽā¯āŽšāŽ¤ā¯āŽ¤ā¯ˆāŽĒā¯ āŽĒāŽąā¯āŽąāŽŋāŽ¯ āŽ•ā¯‚āŽŸā¯āŽ¤āŽ˛ā¯ āŽĩāŽŋāŽĩāŽ°āŽ™ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯, āŽŸāŽžāŽ•ā¯āŽ¸ā¯ āŽāŽĒā¯ āŽĒāŽžāŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯.", + "oauth_settings_more_details": "āŽ‡āŽ¨ā¯āŽ¤ āŽ¨āŽąā¯āŽĒāŽŖā¯āŽĒ❈āŽĒā¯ āŽĒāŽąā¯āŽąāŽŋāŽ¯ āŽ•ā¯‚āŽŸā¯āŽ¤āŽ˛ā¯ āŽĩāŽŋāŽĩāŽ°āŽ™ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯, āŽ†āŽĩāŽŖāŽ™ā¯āŽ•āŽŗā¯ˆ āŽāŽĒā¯ āŽĒāŽžāŽ°ā¯.", "oauth_storage_label_claim": "āŽšā¯‡āŽŽāŽŋāŽĒā¯āŽĒāŽ• āŽ˛ā¯‡āŽĒāŽŋāŽŗā¯ āŽ‰āŽ°āŽŋāŽŽā¯ˆāŽ•ā¯‹āŽ°āŽ˛ā¯", "oauth_storage_label_claim_description": "āŽĒāŽ¯āŽŠāŽ°āŽŋāŽŠā¯ āŽšā¯‡āŽŽāŽŋāŽĒā¯āŽĒāŽ• āŽ˛ā¯‡āŽĒāŽŋāŽŗā¯ˆ āŽ‡āŽ¨ā¯āŽ¤ āŽ‰āŽ°āŽŋāŽŽā¯ˆāŽ•ā¯‹āŽ°āŽ˛āŽŋāŽŠā¯ āŽŽāŽ¤āŽŋāŽĒā¯āŽĒā¯āŽ•ā¯āŽ•ā¯ āŽ¤āŽžāŽŠāŽžāŽ• āŽ…āŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯.", "oauth_storage_quota_claim": "āŽšā¯‡āŽŽāŽŋāŽĒā¯āŽĒāŽ• āŽ’āŽ¤ā¯āŽ•ā¯āŽ•ā¯€āŽŸā¯ āŽ‰āŽ°āŽŋāŽŽā¯ˆāŽ•ā¯‹āŽ°āŽ˛ā¯", @@ -285,10 +297,13 @@ "paths_validated_successfully": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽĒāŽžāŽ¤ā¯ˆāŽ•āŽŗā¯āŽŽā¯ āŽĩā¯†āŽąā¯āŽąāŽŋāŽ•āŽ°āŽŽāŽžāŽ• āŽšāŽ°āŽŋāŽĒāŽžāŽ°ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŠ", "person_cleanup_job": "āŽ¨āŽĒāŽ°ā¯ āŽ¤ā¯‚āŽ¯ā¯āŽŽā¯ˆāŽĒā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤ā¯āŽ¤āŽ˛ā¯", "queue_details": "āŽĩāŽ°āŽŋāŽšā¯ˆ āŽĩāŽŋāŽĩāŽ°āŽ™ā¯āŽ•āŽŗā¯", + "queues": "āŽĩā¯‡āŽ˛ā¯ˆ āŽĩāŽ°āŽŋāŽšā¯ˆāŽ•āŽŗā¯", + "queues_page_description": "āŽ¨āŽŋāŽ°ā¯āŽĩāŽžāŽ•āŽŋ āŽĩā¯‡āŽ˛ā¯ˆ āŽĩāŽ°āŽŋāŽšā¯ˆāŽ•āŽŗā¯ āŽĒāŽ•ā¯āŽ•āŽŽā¯", "quota_size_gib": "āŽ’āŽ¤ā¯āŽ•ā¯āŽ•ā¯€āŽŸā¯ āŽ…āŽŗāŽĩ❁ (GiB)", "refreshing_all_libraries": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽ¨ā¯‚āŽ˛āŽ•āŽ™ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯", "registration": "āŽ¨āŽŋāŽ°ā¯āŽĩāŽžāŽ• āŽĒāŽ¤āŽŋāŽĩ❁", "registration_description": "āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽ•āŽŖāŽŋāŽŠāŽŋāŽ¯āŽŋāŽ˛ā¯ āŽŽā¯āŽ¤āŽ˛ā¯ āŽĒāŽ¯āŽŠāŽ°āŽžāŽ• āŽ‡āŽ°ā¯āŽĒā¯āŽĒāŽ¤āŽžāŽ˛ā¯, āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽ¨āŽŋāŽ°ā¯āŽĩāŽžāŽ•āŽŋāŽ¯āŽžāŽ• āŽ¨āŽŋāŽ¯āŽŽāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽĩā¯€āŽ°ā¯āŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽ¨āŽŋāŽ°ā¯āŽĩāŽžāŽ•āŽĒā¯ āŽĒāŽŖāŽŋāŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯āŽĒā¯ āŽĒā¯ŠāŽąā¯āŽĒā¯āŽĒāŽžāŽĩā¯€āŽ°ā¯āŽ•āŽŗā¯, āŽŽā¯‡āŽ˛ā¯āŽŽā¯ āŽ‰āŽ™ā¯āŽ•āŽŗāŽžāŽ˛ā¯ āŽ•ā¯‚āŽŸā¯āŽ¤āŽ˛ā¯ āŽĒāŽ¯āŽŠāŽ°ā¯āŽ•āŽŗā¯ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽĩāŽžāŽ°ā¯āŽ•āŽŗā¯.", + "remove_failed_jobs": "āŽ¤ā¯‹āŽ˛ā¯āŽĩāŽŋāŽ¯ā¯āŽąā¯āŽą āŽĩā¯‡āŽ˛ā¯ˆāŽ•āŽŗā¯ˆ āŽ…āŽ•āŽąā¯āŽąāŽĩā¯āŽŽā¯", "require_password_change_on_login": "āŽŽā¯āŽ¤āŽ˛ā¯ āŽ‰āŽŗā¯āŽ¨ā¯āŽ´ā¯ˆāŽĩāŽŋāŽ˛ā¯ āŽĒāŽ¯āŽŠāŽ°ā¯ āŽ•āŽŸāŽĩā¯āŽšā¯āŽšā¯ŠāŽ˛ā¯āŽ˛ā¯ˆ āŽŽāŽžāŽąā¯āŽą āŽĩā¯‡āŽŖā¯āŽŸā¯āŽŽā¯", "reset_settings_to_default": "āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽ‡āŽ¯āŽ˛ā¯āŽĒā¯āŽ¨āŽŋāŽ˛ā¯ˆāŽ•ā¯āŽ•ā¯ āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "reset_settings_to_recent_saved": "āŽ…āŽŖā¯āŽŽā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽšā¯‡āŽŽāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -296,7 +311,7 @@ "search_jobs": "āŽĩā¯‡āŽ˛ā¯ˆāŽ•āŽŗā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽŸā¯āŽ™ā¯āŽ•āŽŗā¯â€Ļ", "send_welcome_email": "āŽĩāŽ°āŽĩā¯‡āŽąā¯āŽĒ❁ āŽŽāŽŋāŽŠā¯āŽŠāŽžā¯āŽšāŽ˛ā¯ˆ āŽ…āŽŠā¯āŽĒā¯āŽĒāŽĩā¯āŽŽā¯", "server_external_domain_settings": "āŽĩā¯†āŽŗāŽŋāŽĒā¯āŽĒā¯āŽą āŽ•āŽŗāŽŽā¯", - "server_external_domain_settings_description": "HTTP (āŽ•āŽŗā¯) āŽ‰āŽŸā¯āŽĒāŽŸ āŽĒā¯ŠāŽ¤ā¯ āŽĒāŽ•āŽŋāŽ°āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ‡āŽŖā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•āŽžāŽŠ āŽŸā¯ŠāŽŽā¯ˆāŽŠā¯: //", + "server_external_domain_settings_description": "āŽĩā¯†āŽŗāŽŋāŽĒā¯āŽĒā¯āŽą āŽ‡āŽŖā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯ āŽŸā¯ŠāŽŽā¯ˆāŽŠā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĒā¯āŽĒāŽŸā¯āŽ•āŽŋāŽąāŽ¤ā¯", "server_public_users": "āŽĒā¯ŠāŽ¤ā¯ āŽĒāŽ¯āŽŠāŽ°ā¯āŽ•āŽŗā¯", "server_public_users_description": "āŽĒāŽ•āŽŋāŽ°āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗāŽŋāŽ˛ā¯ āŽĒāŽ¯āŽŠāŽ°ā¯ˆāŽšā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•ā¯āŽŽā¯āŽĒā¯‹āŽ¤ā¯ āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽĒāŽ¯āŽŠāŽ°ā¯āŽ•āŽŗā¯āŽŽā¯ (āŽĒā¯†āŽ¯āŽ°ā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽŽāŽŋāŽŠā¯āŽŠāŽžā¯āŽšāŽ˛ā¯) āŽĒāŽŸā¯āŽŸāŽŋāŽ¯āŽ˛āŽŋāŽŸāŽĒā¯āŽĒāŽŸā¯āŽŸā¯āŽŗā¯āŽŗāŽŠ. āŽŽā¯āŽŸāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽžāŽ˛ā¯, āŽĒāŽ¯āŽŠāŽ°ā¯ āŽĒāŽŸā¯āŽŸāŽŋāŽ¯āŽ˛ā¯ āŽ¨āŽŋāŽ°ā¯āŽĩāŽžāŽ• āŽĒāŽ¯āŽŠāŽ°ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯ āŽŽāŽŸā¯āŽŸā¯āŽŽā¯‡ āŽ•āŽŋāŽŸā¯ˆāŽ•ā¯āŽ•ā¯āŽŽā¯.", "server_settings": "āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ• āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯", @@ -318,8 +333,8 @@ "storage_template_migration_description": "āŽāŽąā¯āŽ•āŽŠāŽĩ❇ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽŋāŽ¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯ āŽ¤āŽąā¯āŽĒā¯‹āŽ¤ā¯ˆāŽ¯ {template} āŽāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", "storage_template_migration_info": "āŽšā¯‡āŽŽāŽŋāŽĒā¯āŽĒāŽ• āŽĩāŽžāŽ°ā¯āŽĒā¯āŽĒā¯āŽ°ā¯ āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽ¨ā¯€āŽŸā¯āŽŸāŽŋāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽšāŽŋāŽąāŽŋāŽ¯ āŽŽāŽ´ā¯āŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯ āŽŽāŽžāŽąā¯āŽąā¯āŽŽā¯. āŽŸā¯†āŽŽā¯āŽĒā¯āŽŗā¯‡āŽŸā¯ āŽŽāŽžāŽąā¯āŽąāŽ™ā¯āŽ•āŽŗā¯ āŽĒā¯āŽ¤āŽŋāŽ¯ āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯ āŽŽāŽŸā¯āŽŸā¯āŽŽā¯‡ āŽĒā¯ŠāŽ°ā¯āŽ¨ā¯āŽ¤ā¯āŽŽā¯. āŽŽā¯āŽŠā¯āŽĒ❁ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽŋāŽ¯ āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯ āŽŸā¯†āŽŽā¯āŽĒā¯āŽŗā¯‡āŽŸā¯āŽŸā¯ˆāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤, {job} āŽ āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯.", "storage_template_migration_job": "āŽ¸ā¯āŽŸā¯‹āŽ°ā¯‡āŽœā¯ āŽŸā¯†āŽŽā¯āŽĒā¯āŽŗā¯‡āŽŸā¯ āŽ‡āŽŸāŽŽā¯āŽĒā¯†āŽ¯āŽ°ā¯āŽĩ❁ āŽĩā¯‡āŽ˛ā¯ˆ", - "storage_template_more_details": "āŽ‡āŽ¨ā¯āŽ¤ āŽ…āŽŽā¯āŽšāŽ¤ā¯āŽ¤ā¯ˆāŽĒā¯ āŽĒāŽąā¯āŽąāŽŋāŽ¯ āŽ•ā¯‚āŽŸā¯āŽ¤āŽ˛ā¯ āŽĩāŽŋāŽĩāŽ°āŽ™ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯, Storage Template āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽ…āŽ¤āŽŠā¯ āŽ¤āŽžāŽ•ā¯āŽ•āŽ™ā¯āŽ•āŽŗā¯ āŽāŽĒā¯ āŽĒāŽžāŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", - "storage_template_onboarding_description_v2": "āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽžāŽ˛ā¯, āŽ‡āŽ¨ā¯āŽ¤ āŽ…āŽŽā¯āŽšāŽŽā¯ āŽĒāŽ¯āŽŠāŽ°ā¯ āŽĩāŽ°ā¯ˆāŽ¯āŽąā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽŸā¯†āŽŽā¯āŽĒā¯āŽŗā¯‡āŽŸā¯āŽŸāŽŋāŽŠā¯ āŽ…āŽŸāŽŋāŽĒā¯āŽĒāŽŸā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽ¤āŽžāŽŠāŽžāŽ• āŽ’āŽ´ā¯āŽ™ā¯āŽ•āŽŽā¯ˆāŽ•ā¯āŽ•ā¯āŽŽā¯. āŽŽā¯‡āŽ˛ā¯āŽŽā¯ āŽ¤āŽ•āŽĩāŽ˛ā¯āŽ•ā¯āŽ•ā¯, āŽ†āŽĩāŽŖāŽ™ā¯āŽ•āŽŗā¯ āŽāŽĒā¯ āŽĒāŽžāŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯.", + "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": "āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽą āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗāŽŋāŽŠā¯ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽąā¯ˆ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒ❁ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽĒā¯†āŽ¯āŽ°ā¯ˆ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -336,7 +351,7 @@ "template_settings": "āŽ…āŽąāŽŋāŽĩāŽŋāŽĒā¯āŽĒ❁ āŽĩāŽžāŽ°ā¯āŽĒā¯āŽĒā¯āŽ°ā¯āŽ•ā¯āŽ•āŽŗā¯", "template_settings_description": "āŽ…āŽąāŽŋāŽĩāŽŋāŽĒā¯āŽĒā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯ āŽ¤āŽŠāŽŋāŽĒā¯āŽĒāŽ¯āŽŠā¯ āŽĩāŽžāŽ°ā¯āŽĒā¯āŽĒā¯āŽ°ā¯āŽ•ā¯āŽ•āŽŗā¯ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "theme_custom_css_settings": "āŽ¤āŽŠāŽŋāŽĒā¯āŽĒāŽ¯āŽŠā¯ CSS", - "theme_custom_css_settings_description": "CSS āŽ…āŽŽā¯āŽšāŽŽā¯ Immich āŽĩāŽŸāŽŋāŽĩāŽŽā¯ˆāŽĒā¯āŽĒ❈ āŽ¤āŽŠāŽŋāŽĒā¯āŽĒāŽ¯āŽŠāŽžāŽ•ā¯āŽ• āŽ…āŽŠā¯āŽŽāŽ¤āŽŋāŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯.", + "theme_custom_css_settings_description": "āŽ…āŽŸā¯āŽ•ā¯āŽ•ā¯ āŽ¨āŽŸā¯ˆ āŽ¤āŽžāŽŗā¯āŽ•āŽŗā¯ āŽ¨āŽąā¯āŽĒāŽŖā¯āŽĒ❈āŽĒā¯ āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯ āŽĩāŽŸāŽŋāŽĩāŽŽā¯ˆāŽĒā¯āŽĒā¯ˆāŽ¤ā¯ āŽ¤āŽŠāŽŋāŽĒā¯āŽĒāŽ¯āŽŠāŽžāŽ•ā¯āŽ• āŽ…āŽŠā¯āŽŽāŽ¤āŽŋāŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯.", "theme_settings": "āŽ¤ā¯€āŽŽā¯ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯", "theme_settings_description": "āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯ āŽĩāŽ˛ā¯ˆ āŽ‡āŽŸā¯ˆāŽŽā¯āŽ•āŽ¤ā¯āŽ¤āŽŋāŽŠā¯ āŽ¤āŽŠāŽŋāŽĒā¯āŽĒāŽ¯āŽŠāŽžāŽ•ā¯āŽ•āŽ¤ā¯āŽ¤ā¯ˆ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "thumbnail_generation_job": "āŽšāŽŋāŽąā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -396,7 +411,7 @@ "transcoding_tone_mapping": "āŽ¤ā¯ŠāŽŠāŽŋ-āŽŽā¯‡āŽĒā¯āŽĒāŽŋāŽ™ā¯", "transcoding_tone_mapping_description": "āŽŽāŽšā¯.āŽŸāŽŋ.āŽ†āŽ°āŽžāŽ• āŽŽāŽžāŽąā¯āŽąāŽĒā¯āŽĒāŽŸā¯āŽŽā¯āŽĒā¯‹āŽ¤ā¯ āŽŽāŽšā¯.āŽŸāŽŋ.āŽ†āŽ°ā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗāŽŋāŽŠā¯ āŽ¤ā¯‹āŽąā¯āŽąāŽ¤ā¯āŽ¤ā¯ˆ āŽĒāŽžāŽ¤ā¯āŽ•āŽžāŽ•ā¯āŽ• āŽŽā¯āŽ¯āŽąā¯āŽšāŽŋāŽ•āŽŗā¯. āŽ’āŽĩā¯āŽĩā¯ŠāŽ°ā¯ āŽĩāŽ´āŽŋāŽŽā¯āŽąā¯ˆāŽ¯ā¯āŽŽā¯ āŽĩāŽŖā¯āŽŖāŽŽā¯, āŽĩāŽŋāŽĩāŽ°āŽŽā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽĒāŽŋāŽ°āŽ•āŽžāŽšāŽ¤ā¯āŽ¤āŽŋāŽąā¯āŽ•ā¯ āŽĩ❆āŽĩā¯āŽĩā¯‡āŽąā¯ āŽĒāŽ°āŽŋāŽŽāŽžāŽąā¯āŽąāŽ™ā¯āŽ•āŽŗā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯. āŽ…āŽĒāŽŋāŽŗā¯ āŽĩāŽŋāŽĩāŽ°āŽ™ā¯āŽ•āŽŗā¯ˆ āŽĒāŽžāŽ¤ā¯āŽ•āŽžāŽ•ā¯āŽ•āŽŋāŽąāŽžāŽ°ā¯, āŽŽā¯ŠāŽĒāŽŋāŽ¯āŽšā¯ āŽ¨āŽŋāŽąāŽ¤ā¯āŽ¤ā¯ˆ āŽĒāŽžāŽ¤ā¯āŽ•āŽžāŽ•ā¯āŽ•āŽŋāŽąāŽžāŽ°ā¯, āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽ°ā¯†āŽ¯ā¯āŽŠā¯āŽ†āŽ°ā¯āŽŸā¯ āŽĒāŽŋāŽ°āŽ•āŽžāŽšāŽ¤ā¯āŽ¤ā¯ˆ āŽĒāŽžāŽ¤ā¯āŽ•āŽžāŽ•ā¯āŽ•āŽŋāŽąāŽžāŽ°ā¯.", "transcoding_transcode_policy": "āŽŸāŽŋāŽ°āŽžāŽŠā¯āŽšā¯āŽ•ā¯‹āŽŸā¯ āŽ•ā¯ŠāŽŗā¯āŽ•ā¯ˆ", - "transcoding_transcode_policy_description": "āŽ’āŽ°ā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹ āŽŽāŽĒā¯āŽĒā¯‹āŽ¤ā¯ āŽŽāŽžāŽąā¯āŽąāŽĒā¯āŽĒāŽŸ āŽĩā¯‡āŽŖā¯āŽŸā¯āŽŽā¯ āŽŽāŽŠā¯āŽĒāŽ¤āŽąā¯āŽ•āŽžāŽŠ āŽ•ā¯ŠāŽŗā¯āŽ•ā¯ˆ. āŽŽāŽšā¯.āŽŸāŽŋ.āŽ†āŽ°ā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ āŽŽāŽĒā¯āŽĒā¯‹āŽ¤ā¯āŽŽā¯ āŽŸāŽŋāŽ°āŽžāŽŠā¯āŽšā¯āŽ•ā¯‹āŽŸā¯ āŽšā¯†āŽ¯ā¯āŽ¯āŽĒā¯āŽĒāŽŸā¯āŽŽā¯ (āŽŸāŽŋāŽ°āŽžāŽŠā¯āŽšā¯āŽ•ā¯‹āŽŸāŽŋāŽ™ā¯ āŽŽā¯āŽŸāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŋāŽ°ā¯āŽ¨ā¯āŽ¤āŽžāŽ˛ā¯ āŽ¤āŽĩāŽŋāŽ°).", + "transcoding_transcode_policy_description": "āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹ āŽŽāŽĒā¯āŽĒā¯‹āŽ¤ā¯ āŽŸāŽŋāŽ°āŽžāŽŠā¯āŽšā¯āŽ•ā¯‹āŽŸā¯ āŽšā¯†āŽ¯ā¯āŽ¯āŽĒā¯āŽĒāŽŸ āŽĩā¯‡āŽŖā¯āŽŸā¯āŽŽā¯ āŽŽāŽŠā¯āŽĒāŽ¤āŽąā¯āŽ•āŽžāŽŠ āŽ•ā¯ŠāŽŗā¯āŽ•ā¯ˆ. HDR āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹ āŽŽāŽąā¯āŽąā¯āŽŽā¯ YUV 4:2:0 āŽ¤āŽĩāŽŋāŽ° āŽĩā¯‡āŽąā¯ āŽĒāŽŸāŽĒā¯āŽĒā¯āŽŗā¯āŽŗāŽŋ āŽĩāŽŸāŽŋāŽĩāŽ¤ā¯āŽ¤ā¯āŽŸāŽŠā¯ āŽ•ā¯‚āŽŸāŽŋāŽ¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ āŽŽāŽĒā¯āŽĒā¯ŠāŽ´ā¯āŽ¤ā¯āŽŽā¯ āŽŸāŽŋāŽ°āŽžāŽŠā¯āŽšā¯āŽ•ā¯‹āŽŸā¯ āŽšā¯†āŽ¯ā¯āŽ¯āŽĒā¯āŽĒāŽŸā¯āŽŽā¯ (āŽŸāŽŋāŽ°āŽžāŽŠā¯āŽšā¯āŽ•ā¯‹āŽŸāŽŋāŽ™ā¯ āŽŽā¯āŽŸāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŋāŽ°ā¯āŽ¨ā¯āŽ¤āŽžāŽ˛ā¯ āŽ¤āŽĩāŽŋāŽ°).", "transcoding_two_pass_encoding": "āŽ‡āŽ°āŽŖā¯āŽŸā¯-āŽĒāŽžāŽšā¯ āŽ•ā¯āŽąāŽŋāŽ¯āŽžāŽ•ā¯āŽ•āŽŽā¯", "transcoding_two_pass_encoding_setting_description": "āŽšāŽŋāŽąāŽ¨ā¯āŽ¤ āŽ•ā¯āŽąāŽŋāŽ¯āŽžāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ• āŽ‡āŽ°āŽŖā¯āŽŸā¯ āŽĒāŽžāŽšā¯āŽ•āŽŗāŽŋāŽ˛ā¯ āŽŸāŽŋāŽ°āŽžāŽŠā¯āŽšā¯āŽ•ā¯‹āŽŸā¯. āŽŽā¯‡āŽ•ā¯āŽšā¯ āŽĒāŽŋāŽŸā¯āŽ°ā¯‡āŽŸā¯ āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŋāŽ°ā¯āŽ•ā¯āŽ•ā¯āŽŽā¯āŽĒā¯‹āŽ¤ā¯ (H.264 āŽŽāŽąā¯āŽąā¯āŽŽā¯ HEVC āŽ‰āŽŸāŽŠā¯ āŽĩā¯‡āŽ˛ā¯ˆ āŽšā¯†āŽ¯ā¯āŽ¯ āŽ‡āŽ¤ā¯ āŽ¤ā¯‡āŽĩ❈āŽĒā¯āŽĒāŽŸā¯āŽ•āŽŋāŽąāŽ¤ā¯), āŽ‡āŽ¨ā¯āŽ¤ āŽĒāŽ¯āŽŠā¯āŽŽā¯āŽąā¯ˆ āŽ…āŽ¤āŽŋāŽ•āŽĒāŽŸā¯āŽš āŽĒāŽŋāŽŸā¯āŽ°ā¯‡āŽŸā¯āŽŸā¯ˆ āŽ…āŽŸāŽŋāŽĒā¯āŽĒāŽŸā¯ˆāŽ¯āŽžāŽ•āŽ•ā¯ āŽ•ā¯ŠāŽŖā¯āŽŸ āŽĒāŽŋāŽŸā¯āŽ°ā¯‡āŽŸā¯ āŽĩāŽ°āŽŽā¯āŽĒ❈āŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤ā¯āŽ•āŽŋāŽąāŽ¤ā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ CRF āŽ āŽĒā¯āŽąāŽ•ā¯āŽ•āŽŖāŽŋāŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯. VP9 āŽāŽĒā¯ āŽĒā¯ŠāŽąā¯āŽ¤ā¯āŽ¤āŽĩāŽ°ā¯ˆ, āŽ…āŽ¤āŽŋāŽ•āŽĒāŽŸā¯āŽš āŽĒāŽŋāŽŸā¯āŽ°ā¯‡āŽŸā¯ āŽŽā¯āŽŸāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŋāŽ°ā¯āŽ¨ā¯āŽ¤āŽžāŽ˛ā¯ CRF āŽāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽ˛āŽžāŽŽā¯.", "transcoding_video_codec": "āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹ āŽ•ā¯‹āŽŸā¯†āŽ•ā¯", @@ -413,7 +428,7 @@ "user_delete_delay": "{user}āŽ‡āŽŠā¯ āŽ•āŽŖāŽ•ā¯āŽ•ā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ {delay, plural, one {# āŽ¨āŽžāŽŗā¯} other {# āŽ¨āŽžāŽŗā¯āŽ•āŽŗā¯}}āŽ‡āŽ˛ā¯ āŽ¨āŽŋāŽ°āŽ¨ā¯āŽ¤āŽ° āŽ¨ā¯€āŽ•ā¯āŽ•āŽ¤ā¯ āŽ¤āŽŋāŽŸā¯āŽŸāŽŽāŽŋāŽŸāŽĒā¯āŽĒāŽŸā¯āŽŽā¯.", "user_delete_delay_settings": "āŽ¤āŽžāŽŽāŽ¤āŽ¤ā¯āŽ¤ā¯ˆ āŽ¨ā¯€āŽ•ā¯āŽ•ā¯", "user_delete_delay_settings_description": "āŽŽāŽŖā¯ of days after āŽ¨ā¯€āŽ•ā¯āŽ•ā¯āŽŽā¯ āŽĒā¯†āŽąā¯āŽ¨āŽ°ā¯ permanently āŽ¨ā¯€āŽ•ā¯āŽ•ā¯ a user's account and assets. āŽ¨ā¯€āŽ•ā¯āŽ•ā¯āŽĩāŽ¤āŽąā¯āŽ•ā¯ āŽ¤āŽ¯āŽžāŽ°āŽžāŽ• āŽ‡āŽ°ā¯āŽ•ā¯āŽ•ā¯āŽŽā¯ āŽĒāŽ¯āŽŠāŽ°ā¯āŽ•āŽŗā¯ˆāŽšā¯ āŽšāŽ°āŽŋāŽĒāŽžāŽ°ā¯āŽ•ā¯āŽ• āŽĒāŽ¯āŽŠāŽ°ā¯ āŽ¨ā¯€āŽ•ā¯āŽ•ā¯āŽ¤āŽ˛ā¯ āŽĩā¯‡āŽ˛ā¯ˆ āŽ¨āŽŗā¯āŽŗāŽŋāŽ°āŽĩāŽŋāŽ˛ā¯ āŽ‡āŽ¯āŽ™ā¯āŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯. āŽ‡āŽ¨ā¯āŽ¤ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒāŽŋāŽ˛ā¯ āŽŽāŽžāŽąā¯āŽąāŽ™ā¯āŽ•āŽŗā¯ āŽ…āŽŸā¯āŽ¤ā¯āŽ¤ āŽŽāŽ°āŽŖāŽ¤āŽŖā¯āŽŸāŽŠā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽŽāŽ¤āŽŋāŽĒā¯āŽĒā¯€āŽŸā¯ āŽšā¯†āŽ¯ā¯āŽ¯āŽĒā¯āŽĒāŽŸā¯āŽŽā¯.", - "user_delete_immediately": " {user} āŽ‡āŽŠā¯ āŽ•āŽŖāŽ•ā¯āŽ•ā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ āŽ¨āŽŋāŽ°āŽ¨ā¯āŽ¤āŽ° āŽ¨ā¯€āŽ•ā¯āŽ•ā¯āŽ¤āŽ˛ā¯āŽ•ā¯āŽ•āŽžāŽ• āŽĩāŽ°āŽŋāŽšā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽ¨āŽŋāŽąā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯ āŽ‰āŽŸāŽŠāŽŸāŽŋāŽ¯āŽžāŽ• .", + "user_delete_immediately": "{user} āŽ‡āŽŠā¯ āŽ•āŽŖāŽ•ā¯āŽ•ā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ āŽ¨āŽŋāŽ°āŽ¨ā¯āŽ¤āŽ° āŽ¨ā¯€āŽ•ā¯āŽ•ā¯āŽ¤āŽ˛ā¯āŽ•ā¯āŽ•āŽžāŽ• āŽĩāŽ°āŽŋāŽšā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽ¨āŽŋāŽąā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯ āŽ‰āŽŸāŽŠāŽŸāŽŋāŽ¯āŽžāŽ•.", "user_delete_immediately_checkbox": "āŽ‰āŽŸāŽŠāŽŸāŽŋāŽ¯āŽžāŽ• āŽ¨ā¯€āŽ•ā¯āŽ• āŽĒāŽ¯āŽŠāŽ°ā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯", "user_details": "āŽĒāŽ¯āŽŠāŽ°ā¯ āŽĩāŽŋāŽĩāŽ°āŽ™ā¯āŽ•āŽŗā¯", "user_management": "āŽĒāŽ¯āŽŠāŽ°ā¯ āŽŽā¯‡āŽ˛āŽžāŽŖā¯āŽŽā¯ˆ", @@ -426,7 +441,7 @@ "user_successfully_removed": "āŽĒāŽ¯āŽŠāŽ°ā¯ {email} āŽĩā¯†āŽąā¯āŽąāŽŋāŽ•āŽ°āŽŽāŽžāŽ• āŽ…āŽ•āŽąā¯āŽąāŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯.", "users_page_description": "āŽ¨āŽŋāŽ°ā¯āŽĩāŽžāŽ• āŽĒāŽ¯āŽŠāŽ°ā¯āŽ•āŽŗā¯ āŽĒāŽ•ā¯āŽ•āŽŽā¯", "version_check_enabled_description": "āŽĒāŽ¤āŽŋāŽĒā¯āŽĒ❁ āŽšāŽ°āŽŋāŽĒāŽžāŽ°ā¯āŽĒā¯āŽĒ❁ āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", - "version_check_implications": "āŽĒāŽ¤āŽŋāŽĒā¯āŽĒ❁ āŽšāŽ°āŽŋāŽĒāŽžāŽ°ā¯āŽĒā¯āŽĒ❁ āŽ…āŽŽā¯āŽšāŽŽā¯ github .com āŽ‰āŽŸāŽŠāŽžāŽŠ āŽ…āŽĩā¯āŽĩāŽĒā¯āŽĒā¯‹āŽ¤ā¯ āŽ¤ā¯ŠāŽŸāŽ°ā¯āŽĒā¯āŽ•ā¯ŠāŽŗā¯āŽĩāŽ¤ā¯ˆ āŽ¨āŽŽā¯āŽĒāŽŋāŽ¯ā¯āŽŗā¯āŽŗāŽ¤ā¯", + "version_check_implications": "āŽĒāŽ¤āŽŋāŽĒā¯āŽĒ❁ āŽšāŽ°āŽŋāŽĒāŽžāŽ°ā¯āŽĒā¯āŽĒ❁ āŽ…āŽŽā¯āŽšāŽŽā¯ {server} āŽ‰āŽŸāŽŠāŽžāŽŠ āŽ…āŽĩā¯āŽĩāŽĒā¯āŽĒā¯‹āŽ¤ā¯ āŽ¤ā¯ŠāŽŸāŽ°ā¯āŽĒā¯āŽ•ā¯ŠāŽŗā¯āŽĩāŽ¤ā¯ˆ āŽ¨āŽŽā¯āŽĒāŽŋāŽ¯ā¯āŽŗā¯āŽŗāŽ¤ā¯", "version_check_settings": "āŽĒāŽ¤āŽŋāŽĒā¯āŽĒ❁ āŽšā¯‹āŽ¤āŽŠā¯ˆ", "version_check_settings_description": "āŽĒā¯āŽ¤āŽŋāŽ¯ āŽĒāŽ¤āŽŋāŽĒā¯āŽĒ❁ āŽ…āŽąāŽŋāŽĩāŽŋāŽĒā¯āŽĒ❈ āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯/āŽŽā¯āŽŸāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "video_conversion_job": "āŽŸāŽŋāŽ°āŽžāŽŠā¯āŽšā¯āŽ•ā¯‹āŽŸā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯", @@ -436,6 +451,9 @@ "admin_password": "āŽ¨āŽŋāŽ°ā¯āŽĩāŽžāŽ•āŽŋ āŽ•āŽŸāŽĩā¯āŽšā¯āŽšā¯ŠāŽ˛ā¯", "administration": "āŽ¨āŽŋāŽ°ā¯āŽĩāŽžāŽ•āŽŽā¯", "advanced": "āŽŽā¯‡āŽŽā¯āŽĒāŽŸā¯āŽŸ", + "advanced_settings_clear_image_cache": "āŽĒāŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽ¤āŽąā¯āŽ•āŽžāŽ˛āŽŋāŽ• āŽšā¯‡āŽŽāŽŋāŽĒā¯āŽĒ❈ āŽ…āŽ´āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "advanced_settings_clear_image_cache_error": "āŽĒāŽŸāŽ¤ā¯āŽ¤āŽŋāŽŠā¯ āŽ¤āŽąā¯āŽ•āŽžāŽ˛āŽŋāŽ• āŽšā¯‡āŽŽāŽŋāŽĒā¯āŽĒ❈ āŽ…āŽ´āŽŋāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", + "advanced_settings_clear_image_cache_success": "{size} āŽĩā¯†āŽąā¯āŽąāŽŋāŽ•āŽ°āŽŽāŽžāŽ• āŽ…āŽ´āŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "advanced_settings_enable_alternate_media_filter_subtitle": "āŽŽāŽžāŽąā¯āŽąā¯ āŽ…āŽŗāŽĩā¯āŽ•ā¯‹āŽ˛ā¯āŽ•āŽŗāŽŋāŽŠā¯ āŽ…āŽŸāŽŋāŽĒā¯āŽĒāŽŸā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽ’āŽ¤ā¯āŽ¤āŽŋāŽšā¯ˆāŽĩāŽŋāŽŠā¯ āŽĒā¯‹āŽ¤ā¯ āŽŽā¯€āŽŸāŽŋāŽ¯āŽžāŽĩ❈ āŽĩāŽŸāŽŋāŽ•āŽŸā¯āŽŸ āŽ‡āŽ¨ā¯āŽ¤ āŽĩāŽŋāŽ°ā¯āŽĒā¯āŽĒāŽ¤ā¯āŽ¤ā¯ˆāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯. āŽŽāŽ˛ā¯āŽ˛āŽž āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽ†āŽĒā¯āŽ¸ā¯ āŽ•āŽŖā¯āŽŸāŽąāŽŋāŽĩāŽ¤āŽŋāŽ˛ā¯ āŽšāŽŋāŽ•ā¯āŽ•āŽ˛ā¯āŽ•āŽŗā¯ āŽ‡āŽ°ā¯āŽ¨ā¯āŽ¤āŽžāŽ˛ā¯ āŽŽāŽŸā¯āŽŸā¯āŽŽā¯‡ āŽ‡āŽ¤ā¯ˆ āŽŽā¯āŽ¯āŽąā¯āŽšāŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯.", "advanced_settings_enable_alternate_media_filter_title": "[āŽĒāŽ°āŽŋāŽšā¯‹āŽ¤āŽŠā¯ˆāŽ•ā¯āŽ•ā¯ āŽ‰āŽŸā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯] āŽŽāŽžāŽąā¯āŽąā¯ āŽšāŽžāŽ¤āŽŠ āŽ†āŽ˛ā¯āŽĒ āŽ’āŽ¤ā¯āŽ¤āŽŋāŽšā¯ˆāŽĩ❁ āŽĩāŽŸāŽŋāŽĒā¯āŽĒāŽžāŽŠā¯ˆāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", "advanced_settings_log_level_title": "āŽĒāŽ¤āŽŋāŽĩ❁ āŽ¨āŽŋāŽ˛ā¯ˆ: {level}", @@ -472,10 +490,12 @@ "album_remove_user": "āŽĒāŽ¯āŽŠāŽ°ā¯ˆ āŽ…āŽ•āŽąā¯āŽąāŽĩāŽž?", "album_remove_user_confirmation": "{user} āŽ āŽ…āŽ•āŽąā¯āŽą āŽĩāŽŋāŽ°ā¯āŽŽā¯āŽĒā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗāŽž?", "album_search_not_found": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ¤ā¯‡āŽŸāŽ˛ā¯āŽŸāŽŠā¯ āŽĒā¯ŠāŽ°ā¯āŽ¨ā¯āŽ¤āŽ•ā¯āŽ•ā¯‚āŽŸāŽŋāŽ¯ āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽ¤ā¯āŽĩā¯āŽŽā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ", + "album_selected": "āŽ†āŽ˛ā¯āŽĒāŽŽā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "album_share_no_users": "āŽ‡āŽ¨ā¯āŽ¤ āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤ā¯ˆ āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽ˛ā¯āŽ˛āŽž āŽĒāŽ¯āŽŠāŽ°ā¯āŽ•āŽŗā¯āŽŸāŽŠā¯āŽŽā¯ āŽĒāŽ•āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽ•ā¯ŠāŽŖā¯āŽŸāŽ¤āŽžāŽ•āŽ¤ā¯ āŽ¤ā¯†āŽ°āŽŋāŽ•āŽŋāŽąāŽ¤ā¯ āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽĒāŽ•āŽŋāŽ°ā¯āŽĩāŽ¤āŽąā¯āŽ•ā¯ āŽ‰āŽ™ā¯āŽ•āŽŗāŽŋāŽŸāŽŽā¯ āŽŽāŽ¨ā¯āŽ¤ āŽĒāŽ¯āŽŠāŽ°ā¯āŽŽā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ.", "album_summary": "āŽ†āŽ˛ā¯āŽĒāŽŽā¯ āŽšā¯āŽ°ā¯āŽ•ā¯āŽ•āŽŽā¯", "album_updated": "āŽ†āŽ˛ā¯āŽĒāŽŽā¯ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "album_updated_setting_description": "āŽĒāŽ•āŽŋāŽ°āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽĒā¯āŽ¤āŽŋāŽ¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ āŽ‡āŽ°ā¯āŽ•ā¯āŽ•ā¯āŽŽā¯āŽĒā¯‹āŽ¤ā¯ āŽŽāŽŋāŽŠā¯āŽŠāŽžā¯āŽšāŽ˛ā¯ āŽ…āŽąāŽŋāŽĩāŽŋāŽĒā¯āŽĒ❈āŽĒā¯ āŽĒā¯†āŽąā¯āŽ™ā¯āŽ•āŽŗā¯", + "album_upload_assets": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ•āŽŖāŽŋāŽŠāŽŋāŽ¯āŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•āŽŗā¯ˆāŽĒā¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽŋ āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "album_user_left": "āŽ‡āŽŸāŽ¤ā¯ {album}", "album_user_removed": "āŽ…āŽ•āŽąā¯āŽąāŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯ {user}", "album_viewer_appbar_delete_confirm": "āŽ‡āŽ¨ā¯āŽ¤ āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤ā¯ˆ āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ•āŽŖāŽ•ā¯āŽ•āŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽ¨ā¯€āŽ•ā¯āŽ• āŽĩāŽŋāŽ°ā¯āŽŽā¯āŽĒā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗāŽž?", @@ -493,9 +513,11 @@ "albums_default_sort_order_description": "āŽĒā¯āŽ¤āŽŋāŽ¯ āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•ā¯āŽŽā¯āŽĒā¯‹āŽ¤ā¯ āŽ†āŽ°āŽŽā¯āŽĒ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽĩāŽ°āŽŋāŽšā¯ˆāŽĒā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽ˛ā¯ āŽĩāŽ°āŽŋāŽšā¯ˆ.", "albums_feature_description": "āŽĒāŽŋāŽą āŽĒāŽ¯āŽŠāŽ°ā¯āŽ•āŽŗā¯āŽŸāŽŠā¯ āŽĒāŽ•āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽ•ā¯ŠāŽŗā¯āŽŗāŽ•ā¯āŽ•ā¯‚āŽŸāŽŋāŽ¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗāŽŋāŽŠā¯ āŽ¤ā¯ŠāŽ•ā¯āŽĒā¯āŽĒā¯āŽ•āŽŗā¯.", "albums_on_device_count": "āŽšāŽžāŽ¤āŽŠāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ ({count})", + "albums_selected": "{count, plural, one {# āŽ†āŽ˛ā¯āŽĒāŽŽā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯} other {# āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŠ}}", "all": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯āŽŽā¯", "all_albums": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯āŽŽā¯", "all_people": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽŽāŽ•ā¯āŽ•āŽŗā¯āŽŽā¯", + "all_photos": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯āŽŽā¯", "all_videos": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯āŽŽā¯", "allow_dark_mode": "āŽ‡āŽ°ā¯āŽŖā¯āŽŸ āŽĒāŽ¯āŽŠā¯āŽŽā¯āŽąā¯ˆāŽ¯ā¯ˆ āŽ…āŽŠā¯āŽŽāŽ¤āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "allow_edits": "āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤āŽ™ā¯āŽ•āŽŗā¯ˆ āŽ…āŽŠā¯āŽŽāŽ¤āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -503,6 +525,9 @@ "allow_public_user_to_upload": "āŽĒā¯ŠāŽ¤ā¯ āŽĒāŽ¯āŽŠāŽ°ā¯ˆ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽą āŽ…āŽŠā¯āŽŽāŽ¤āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "allowed": "āŽ…āŽŠā¯āŽŽāŽ¤āŽŋāŽ¤ā¯āŽ¤", "alt_text_qr_code": "QR āŽ•ā¯āŽąāŽŋāŽ¯ā¯€āŽŸā¯ āŽĒāŽŸāŽŽā¯", + "always_keep": "āŽŽāŽĒā¯āŽĒā¯‹āŽ¤ā¯āŽŽā¯ āŽĩā¯ˆāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ™ā¯āŽ•āŽŗā¯", + "always_keep_photos_hint": "āŽ‡āŽ¨ā¯āŽ¤āŽšā¯ āŽšāŽžāŽ¤āŽŠāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽ‰āŽŗā¯āŽŗ āŽŽāŽ˛ā¯āŽ˛āŽžāŽĒā¯ āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽ•āŽžāŽ˛āŽŋāŽ¯āŽžāŽ•ā¯āŽ•ā¯āŽ™ā¯āŽ•āŽŗā¯.", + "always_keep_videos_hint": "āŽ‡āŽ¨ā¯āŽ¤āŽšā¯ āŽšāŽžāŽ¤āŽŠāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽŽāŽ˛ā¯āŽ˛āŽž āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽ•āŽžāŽ˛āŽŋāŽ¯āŽžāŽ•ā¯āŽ•ā¯āŽŽā¯.", "anti_clockwise": "āŽ•āŽŸāŽŋāŽ•āŽžāŽ° āŽŽāŽ¤āŽŋāŽ°ā¯āŽĒā¯āŽĒ❁", "api_key": "āŽĒāŽ¨āŽŋāŽ‡ āŽĩāŽŋāŽšā¯ˆ", "api_key_description": "āŽ‡āŽ¨ā¯āŽ¤ āŽŽāŽ¤āŽŋāŽĒā¯āŽĒ❁ āŽ’āŽ°ā¯ āŽŽā¯āŽąā¯ˆ āŽŽāŽŸā¯āŽŸā¯āŽŽā¯‡ āŽ•āŽžāŽŖā¯āŽĒāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯. āŽšāŽžāŽŗāŽ°āŽ¤ā¯āŽ¤ā¯ˆ āŽŽā¯‚āŽŸā¯āŽĩāŽ¤āŽąā¯āŽ•ā¯ āŽŽā¯āŽŠā¯ āŽ…āŽ¤ā¯ˆ āŽ¨āŽ•āŽ˛ā¯†āŽŸā¯āŽ•ā¯āŽ• āŽŽāŽąāŽ•ā¯āŽ•āŽžāŽ¤ā¯€āŽ°ā¯āŽ•āŽŗā¯.", @@ -529,10 +554,12 @@ "archived_count": "{count, plural, other {āŽ•āŽžāŽĒā¯āŽĒāŽ•āŽĒā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯ #}}", "are_these_the_same_person": "āŽ‡āŽĩāŽ°ā¯āŽ•āŽŗā¯ āŽ’āŽ°ā¯‡ āŽ¨āŽĒāŽ°āŽž?", "are_you_sure_to_do_this": "āŽ‡āŽ¤ā¯ˆ āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽšā¯†āŽ¯ā¯āŽ¯ āŽĩāŽŋāŽ°ā¯āŽŽā¯āŽĒā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗāŽž?", + "array_field_not_fully_supported": "āŽĩāŽ°āŽŋāŽšā¯ˆ āŽĒā¯āŽ˛āŽ™ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯ āŽ•ā¯ˆāŽŽā¯āŽąā¯ˆāŽ¯āŽžāŽ• āŽšāŽžāŽ¤ā¯ŠāŽĒā¯ŠāŽ•ā¯ āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤āŽŽā¯ āŽ¤ā¯‡āŽĩ❈", "asset_action_delete_err_read_only": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ (āŽ•āŽŗā¯ˆ) āŽŽāŽŸā¯āŽŸā¯āŽŽā¯‡ āŽĒāŽŸāŽŋāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽ¤ā¯", "asset_action_share_err_offline": "āŽ‡āŽŖā¯ˆāŽĒā¯āŽĒāŽŋāŽ˛ā¯āŽ˛āŽžāŽ¤ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ (āŽ•āŽŗā¯ˆ) āŽĒā¯†āŽą āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽ¤ā¯, āŽ¤āŽĩāŽŋāŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "asset_added_to_album": "āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "asset_adding_to_album": "āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯â€Ļ", + "asset_created": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "asset_description_updated": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽĩāŽŋāŽŗāŽ•ā¯āŽ•āŽŽā¯ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸā¯āŽŗā¯āŽŗāŽ¤ā¯", "asset_filename_is_offline": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ {filename} āŽ†āŽƒāŽĒā¯āŽ˛ā¯ˆāŽŠāŽŋāŽ˛ā¯ āŽ‰āŽŗā¯āŽŗāŽ¤ā¯", "asset_has_unassigned_faces": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽ’āŽ¤ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽžāŽ¤ āŽŽā¯āŽ•āŽ™ā¯āŽ•āŽŗā¯ˆāŽ•ā¯ āŽ•ā¯ŠāŽŖā¯āŽŸā¯āŽŗā¯āŽŗāŽ¤ā¯", @@ -545,6 +572,9 @@ "asset_list_layout_sub_title": "āŽŽāŽŠā¯ˆāŽ¯āŽŽā¯ˆāŽĩ❁", "asset_list_settings_subtitle": "āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸ āŽ•āŽŸā¯āŽŸāŽŽā¯ āŽ¤āŽŗāŽĩāŽŽā¯ˆāŽĒā¯āŽĒ❁ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯", "asset_list_settings_title": "āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸ āŽ•āŽŸā¯āŽŸāŽŽā¯", + "asset_not_found_on_device_android": "āŽšāŽžāŽ¤āŽŠāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ", + "asset_not_found_on_device_ios": "āŽšāŽžāŽ¤āŽŠāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ. āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ iCloud āŽāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤ā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗā¯ āŽŽāŽŠāŽŋāŽ˛ā¯, iCloud āŽ‡āŽ˛ā¯ āŽšā¯‡āŽŽāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸā¯āŽŗā¯āŽŗ āŽŽā¯‹āŽšāŽŽāŽžāŽŠ āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽ•āŽžāŽ°āŽŖāŽŽāŽžāŽ• āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ˆ āŽ…āŽŖā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽŽāŽ˛ā¯ āŽĒā¯‹āŽ•āŽ˛āŽžāŽŽā¯", + "asset_not_found_on_icloud": "iCloud āŽ‡āŽ˛ā¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽ•āŽžāŽŖāŽĒā¯āŽĒāŽŸāŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ. iCloud āŽ‡āŽ˛ā¯ āŽšā¯‡āŽŽāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸā¯āŽŗā¯āŽŗ āŽŽā¯‹āŽšāŽŽāŽžāŽŠ āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽ•āŽžāŽ°āŽŖāŽŽāŽžāŽ• āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽ…āŽŖā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽ¤āŽ¤āŽžāŽ• āŽ‡āŽ°ā¯āŽ•ā¯āŽ•āŽ˛āŽžāŽŽā¯", "asset_offline": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽ†āŽƒāŽĒā¯āŽ˛ā¯ˆāŽŠāŽŋāŽ˛ā¯", "asset_offline_description": "āŽ‡āŽ¨ā¯āŽ¤ āŽĩā¯†āŽŗāŽŋāŽĒā¯āŽĒā¯āŽą āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽ‡āŽŠāŽŋ āŽĩāŽŸā¯āŽŸāŽŋāŽ˛ā¯ āŽ•āŽžāŽŖāŽĒā¯āŽĒāŽŸāŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ. āŽ‰āŽ¤āŽĩāŽŋāŽ•ā¯āŽ•ā¯ āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯ āŽ¨āŽŋāŽ°ā¯āŽĩāŽžāŽ•āŽŋāŽ¯ā¯ˆ āŽ¤ā¯ŠāŽŸāŽ°ā¯āŽĒ❁ āŽ•ā¯ŠāŽŗā¯āŽŗāŽĩā¯āŽŽā¯.", "asset_restored_successfully": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽĩā¯†āŽąā¯āŽąāŽŋāŽ•āŽ°āŽŽāŽžāŽ• āŽŽā¯€āŽŸā¯āŽŸā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", @@ -596,7 +626,7 @@ "backup_album_selection_page_select_albums": "āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "backup_album_selection_page_selection_info": "āŽ¤ā¯‡āŽ°ā¯āŽĩ❁ āŽšā¯†āŽ¯ā¯āŽ¤āŽŋ", "backup_album_selection_page_total_assets": "āŽŽā¯ŠāŽ¤ā¯āŽ¤ āŽ¤āŽŠāŽŋāŽ¤ā¯āŽ¤ā¯āŽĩāŽŽāŽžāŽŠ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯", - "backup_albums_sync": "āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ āŽ’āŽ¤ā¯āŽ¤āŽŋāŽšā¯ˆāŽĩ❁", + "backup_albums_sync": "āŽ•āŽžāŽĒā¯āŽĒ❁ āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ āŽ’āŽ¤ā¯āŽ¤āŽŋāŽšā¯ˆāŽĩ❁", "backup_all": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯āŽŽā¯", "backup_background_service_backup_failed_message": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ˆ āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽŽāŽŸā¯āŽ•ā¯āŽ•āŽ¤ā¯ āŽ¤āŽĩāŽąāŽŋāŽĩāŽŋāŽŸā¯āŽŸāŽ¤ā¯. āŽŽā¯€āŽŖā¯āŽŸā¯āŽŽā¯ āŽŽā¯āŽ¯āŽąā¯āŽšāŽŋāŽĒā¯āŽĒāŽ¤ā¯â€Ļ", "backup_background_service_complete_notification": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽŽā¯āŽŸāŽŋāŽ¨ā¯āŽ¤āŽ¤ā¯", @@ -657,6 +687,7 @@ "backup_options_page_title": "āŽ•āŽžāŽĒā¯āŽĒ❁ āŽĩāŽŋāŽ°ā¯āŽĒā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯", "backup_setting_subtitle": "āŽĒāŽŋāŽŠā¯āŽŠāŽŖāŽŋ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽŽā¯āŽŠā¯āŽĒā¯āŽą āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽą āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "backup_settings_subtitle": "āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽą āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "backup_upload_details_page_more_details": "āŽŽā¯‡āŽ˛ā¯āŽŽā¯ āŽĩāŽŋāŽĩāŽ°āŽ™ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯ āŽ¤āŽŸā¯āŽŸāŽĩā¯āŽŽā¯", "backward": "āŽĒāŽŋāŽŠā¯āŽŠā¯‹āŽ•ā¯āŽ•ā¯", "biometric_auth_enabled": "āŽĒāŽ¯ā¯‹āŽŽā¯†āŽŸā¯āŽ°āŽŋāŽ•ā¯ āŽāŽąā¯āŽĒ❁ āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "biometric_locked_out": "āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽĒāŽ¯ā¯‹āŽŽā¯†āŽŸā¯āŽ°āŽŋāŽ•ā¯ āŽ…āŽ™ā¯āŽ•ā¯€āŽ•āŽžāŽ°āŽ¤ā¯āŽ¤āŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽĒā¯‚āŽŸā¯āŽŸāŽĒā¯āŽĒāŽŸā¯āŽŸāŽŋāŽ°ā¯āŽ•ā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗā¯", @@ -715,6 +746,8 @@ "change_password_form_password_mismatch": "āŽ•āŽŸāŽĩā¯āŽšā¯āŽšā¯ŠāŽąā¯āŽ•āŽŗā¯ āŽĒā¯ŠāŽ°ā¯āŽ¨ā¯āŽ¤āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "change_password_form_reenter_new_password": "āŽĒā¯āŽ¤āŽŋāŽ¯ āŽ•āŽŸāŽĩā¯āŽšā¯āŽšā¯ŠāŽ˛ā¯āŽ˛ā¯ˆ āŽŽā¯€āŽŖā¯āŽŸā¯āŽŽā¯ āŽ‰āŽŗā¯āŽŗāŽŋāŽŸāŽĩā¯āŽŽā¯", "change_pin_code": "āŽŽā¯āŽŗā¯ āŽ•ā¯āŽąāŽŋāŽ¯ā¯€āŽŸā¯āŽŸā¯ˆ āŽŽāŽžāŽąā¯āŽąāŽĩā¯āŽŽā¯", + "change_trigger": "āŽ¤ā¯‚āŽŖā¯āŽŸā¯āŽ¤āŽ˛ā¯ˆ āŽŽāŽžāŽąā¯āŽąāŽĩā¯āŽŽā¯", + "change_trigger_prompt": "āŽ¤ā¯‚āŽŖā¯āŽŸā¯āŽ¤āŽ˛ā¯ˆ āŽ¨āŽŋāŽšā¯āŽšāŽ¯āŽŽāŽžāŽ• āŽŽāŽžāŽąā¯āŽą āŽĩāŽŋāŽ°ā¯āŽŽā¯āŽĒā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗāŽž? āŽ‡āŽ¤ā¯ āŽāŽąā¯āŽ•āŽŠāŽĩ❇ āŽ‰āŽŗā¯āŽŗ āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽšā¯†āŽ¯āŽ˛ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽĩāŽŸāŽŋāŽĒā¯āŽĒāŽžāŽŠā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽ…āŽ•āŽąā¯āŽąā¯āŽŽā¯.", "change_your_password": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ•āŽŸāŽĩā¯āŽšā¯āŽšā¯ŠāŽ˛ā¯āŽ˛ā¯ˆ āŽŽāŽžāŽąā¯āŽąāŽĩā¯āŽŽā¯", "changed_visibility_successfully": "āŽ¤ā¯†āŽ°āŽŋāŽĩā¯āŽ¨āŽŋāŽ˛ā¯ˆ āŽĩā¯†āŽąā¯āŽąāŽŋāŽ•āŽ°āŽŽāŽžāŽ• āŽŽāŽžāŽąā¯āŽąāŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "charging": "āŽšāŽžāŽ°ā¯āŽšāŽŋāŽ™ā¯", @@ -723,8 +756,21 @@ "check_corrupt_asset_backup_button": "āŽ•āŽžāŽšā¯‹āŽ˛ā¯ˆ āŽšā¯†āŽ¯ā¯āŽ¯ā¯āŽ™ā¯āŽ•āŽŗā¯", "check_corrupt_asset_backup_description": "āŽ‡āŽ¨ā¯āŽ¤ āŽ•āŽžāŽšā¯‹āŽ˛ā¯ˆāŽ¯ā¯ˆ āŽĩā¯ˆāŽƒāŽĒ❈ āŽŽā¯€āŽ¤ā¯ āŽŽāŽŸā¯āŽŸā¯āŽŽā¯‡ āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯, āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯āŽŽā¯ āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯ āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽŽāŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽĩā¯āŽŸāŽŠā¯. āŽšā¯†āŽ¯āŽ˛ā¯āŽŽā¯āŽąā¯ˆ āŽšāŽŋāŽ˛ āŽ¨āŽŋāŽŽāŽŋāŽŸāŽ™ā¯āŽ•āŽŗā¯ āŽ†āŽ•āŽ˛āŽžāŽŽā¯.", "check_logs": "āŽĒāŽ¤āŽŋāŽĩā¯āŽ•āŽŗā¯ˆ āŽšāŽ°āŽŋāŽĒāŽžāŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "checksum": "āŽšā¯†āŽ•ā¯āŽšāŽŽā¯", "choose_matching_people_to_merge": "āŽ’āŽŠā¯āŽąāŽŋāŽŖā¯ˆāŽ•ā¯āŽ• āŽĒā¯ŠāŽ°ā¯āŽ¨ā¯āŽ¤āŽ•ā¯āŽ•ā¯‚āŽŸāŽŋāŽ¯ āŽ¨āŽĒāŽ°ā¯āŽ•āŽŗā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽĩā¯āŽšā¯†āŽ¯ā¯āŽ•", "city": "āŽ¨āŽ•āŽ°āŽŽā¯", + "cleanup_confirm_description": "āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯ {count} āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ˆ ({date} āŽ•ā¯āŽ•ā¯ āŽŽā¯āŽŠā¯ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯) āŽĒāŽžāŽ¤ā¯āŽ•āŽžāŽĒā¯āŽĒāŽžāŽ• āŽšāŽ°ā¯āŽĩāŽ°āŽŋāŽ˛ā¯ āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯ āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽŽāŽŸā¯āŽ¤ā¯āŽ¤ā¯āŽŗā¯āŽŗāŽžāŽ°ā¯. āŽ‡āŽ¨ā¯āŽ¤āŽšā¯ āŽšāŽžāŽ¤āŽŠāŽ¤ā¯āŽ¤āŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽ‰āŽŗā¯āŽŗāŽ• āŽ¨āŽ•āŽ˛ā¯āŽ•āŽŗā¯ˆ āŽ…āŽ•āŽąā¯āŽąāŽĩāŽž?", + "cleanup_confirm_prompt_title": "āŽ‡āŽ¨ā¯āŽ¤āŽšā¯ āŽšāŽžāŽ¤āŽŠāŽ¤ā¯āŽ¤āŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽ…āŽ•āŽąā¯āŽąāŽĩāŽž?", + "cleanup_deleted_assets": "{count} āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ āŽšāŽžāŽ¤āŽŠāŽ¤ā¯āŽ¤āŽŋāŽŠā¯ āŽ•ā¯āŽĒā¯āŽĒā¯ˆāŽ•ā¯āŽ•ā¯ āŽ¨āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŠ", + "cleanup_deleting": "āŽ•ā¯āŽĒā¯āŽĒā¯ˆāŽ•ā¯āŽ•ā¯ āŽ¨āŽ•āŽ°ā¯āŽ•āŽŋāŽąāŽ¤ā¯...", + "cleanup_found_assets": "āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯ āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽŽāŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•āŽŗā¯ {count} āŽ•āŽŖā¯āŽŸāŽąāŽŋāŽ¯āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŠ", + "cleanup_found_assets_with_size": "{count} āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯ āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽŽāŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•āŽŗā¯ ({size}) āŽ•āŽŖā¯āŽŸāŽąāŽŋāŽ¯āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŠ", + "cleanup_icloud_shared_albums_excluded": "iCloud āŽĒāŽ•āŽŋāŽ°āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ āŽĩāŽ°ā¯āŽŸā¯ āŽšā¯†āŽ¯ā¯āŽĩāŽ¤āŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽĩāŽŋāŽ˛āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸā¯āŽŗā¯āŽŗāŽŠ", + "cleanup_no_assets_found": "āŽŽā¯‡āŽ˛ā¯‡ āŽ‰āŽŗā¯āŽŗ āŽ…āŽŗāŽĩā¯āŽ•ā¯‹āŽ˛ā¯āŽ•āŽŗā¯āŽŸāŽŠā¯ āŽĒā¯ŠāŽ°ā¯āŽ¨ā¯āŽ¤āŽ•ā¯āŽ•ā¯‚āŽŸāŽŋāŽ¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ āŽŽāŽ¤ā¯āŽĩā¯āŽŽā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ. āŽ‡āŽŸāŽ¤ā¯āŽ¤ā¯ˆāŽ•ā¯ āŽ•āŽžāŽ˛āŽŋāŽ¯āŽžāŽ•ā¯āŽ•āŽŋāŽŠāŽžāŽ˛ā¯, āŽšāŽ°ā¯āŽĩāŽ°āŽŋāŽ˛ā¯ āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯ āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽŽāŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ˆ āŽŽāŽŸā¯āŽŸā¯āŽŽā¯‡ āŽ…āŽ•āŽąā¯āŽą āŽŽā¯āŽŸāŽŋāŽ¯ā¯āŽŽā¯", + "cleanup_preview_title": "āŽ…āŽ•āŽąā¯āŽą āŽĩā¯‡āŽŖā¯āŽŸāŽŋāŽ¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ ({count})", + "cleanup_step3_description": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ¤ā¯‡āŽ¤āŽŋāŽ¯ā¯āŽŸāŽŠā¯ āŽĒā¯ŠāŽ°ā¯āŽ¨ā¯āŽ¤āŽ•ā¯āŽ•ā¯‚āŽŸāŽŋāŽ¯ āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ˆ āŽĩāŽ°ā¯āŽŸā¯ āŽšā¯†āŽ¯ā¯āŽ¤ā¯ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽĩā¯ˆāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ™ā¯āŽ•āŽŗā¯.", + "cleanup_step4_summary": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ‰āŽŗā¯āŽŗāŽ• āŽšāŽžāŽ¤āŽŠāŽ¤ā¯āŽ¤āŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽ…āŽ•āŽąā¯āŽą {count} āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ ({date}āŽ•ā¯āŽ•ā¯ āŽŽā¯āŽŠā¯ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯). āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯āŽŸāŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ˆ āŽ…āŽŖā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯ā¯āŽŽā¯.", + "cleanup_trash_hint": "āŽšā¯‡āŽŽāŽŋāŽĒā¯āŽĒāŽ• āŽ‡āŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽŽā¯āŽ´ā¯āŽŽā¯ˆāŽ¯āŽžāŽ• āŽŽā¯€āŽŸā¯āŽŸā¯†āŽŸā¯āŽ•ā¯āŽ•, āŽšāŽŋāŽšā¯āŽŸāŽŽā¯ āŽ•ā¯‡āŽ˛āŽ°āŽŋ āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯āŽŸā¯ˆāŽ¤ā¯ āŽ¤āŽŋāŽąāŽ¨ā¯āŽ¤ā¯ āŽ•ā¯āŽĒā¯āŽĒā¯ˆāŽ¯ā¯ˆ āŽĩā¯†āŽąā¯āŽŽā¯ˆ āŽšā¯†āŽ¯ā¯āŽ¯āŽĩā¯āŽŽā¯", "clear": "āŽ¤ā¯†āŽŗāŽŋāŽĩāŽžāŽŠ", "clear_all": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ˆāŽ¯ā¯āŽŽā¯ āŽ…āŽ´āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "clear_all_recent_searches": "āŽ…āŽŖā¯āŽŽā¯ˆāŽ•ā¯ āŽ•āŽžāŽ˛ āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽ¤ā¯‡āŽŸāŽ˛ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽ…āŽ´āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -736,6 +782,8 @@ "client_cert_import": "āŽ‡āŽąāŽ•ā¯āŽ•ā¯āŽŽāŽ¤āŽŋ", "client_cert_import_success_msg": "āŽ•āŽŋāŽŗā¯ˆāŽ¯āŽŠā¯āŽŸā¯ āŽšāŽžāŽŠā¯āŽąāŽŋāŽ¤āŽ´ā¯ āŽ‡āŽąāŽ•ā¯āŽ•ā¯āŽŽāŽ¤āŽŋ āŽšā¯†āŽ¯ā¯āŽ¯āŽĒā¯āŽĒāŽŸā¯āŽ•āŽŋāŽąāŽ¤ā¯", "client_cert_invalid_msg": "āŽ¤āŽĩāŽąāŽžāŽŠ āŽšāŽžāŽŠā¯āŽąāŽŋāŽ¤āŽ´ā¯ āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽ¤āŽĩāŽąāŽžāŽŠ āŽ•āŽŸāŽĩā¯āŽšā¯āŽšā¯ŠāŽ˛ā¯", + "client_cert_password_message": "āŽ‡āŽ¨ā¯āŽ¤ āŽšāŽžāŽŠā¯āŽąāŽŋāŽ¤āŽ´ā¯āŽ•ā¯āŽ•āŽžāŽŠ āŽ•āŽŸāŽĩā¯āŽšā¯āŽšā¯ŠāŽ˛ā¯āŽ˛ā¯ˆ āŽ‰āŽŗā¯āŽŗāŽŋāŽŸāŽĩā¯āŽŽā¯", + "client_cert_password_title": "āŽšāŽžāŽŠā¯āŽąāŽŋāŽ¤āŽ´ā¯ āŽ•āŽŸāŽĩā¯āŽšā¯āŽšā¯ŠāŽ˛ā¯", "client_cert_remove_msg": "āŽ•āŽŋāŽŗā¯ˆāŽ¯āŽŠā¯āŽŸā¯ āŽšāŽžāŽŠā¯āŽąāŽŋāŽ¤āŽ´ā¯ āŽ…āŽ•āŽąā¯āŽąāŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "client_cert_subtitle": "PKCS12 (.p12, .pfx) āŽĩāŽŸāŽŋāŽĩāŽŽā¯ˆāŽĒā¯āŽĒ❈ āŽŽāŽŸā¯āŽŸā¯āŽŽā¯‡ āŽ†āŽ¤āŽ°āŽŋāŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯. āŽ‰āŽŗā¯āŽ¨ā¯āŽ´ā¯ˆāŽĩāŽ¤āŽąā¯āŽ•ā¯ āŽŽā¯āŽŠā¯ āŽŽāŽŸā¯āŽŸā¯āŽŽā¯‡ āŽšāŽžāŽŠā¯āŽąāŽŋāŽ¤āŽ´ā¯ āŽ‡āŽąāŽ•ā¯āŽ•ā¯āŽŽāŽ¤āŽŋ/āŽ…āŽ•āŽąā¯āŽąā¯āŽ¤āŽ˛ā¯ āŽ•āŽŋāŽŸā¯ˆāŽ•ā¯āŽ•ā¯āŽŽā¯", "client_cert_title": "SSL āŽ•āŽŋāŽŗā¯ˆāŽ¯āŽŠā¯āŽŸā¯ āŽšāŽžāŽŠā¯āŽąāŽŋāŽ¤āŽ´ā¯ [āŽĒāŽ°āŽŋāŽšā¯‹āŽ¤āŽŠā¯ˆ]", @@ -746,6 +794,11 @@ "color": "āŽ¨āŽŋāŽąāŽŽā¯", "color_theme": "āŽĩāŽŖā¯āŽŖ āŽ•āŽ°ā¯āŽĒā¯āŽĒā¯ŠāŽ°ā¯āŽŗā¯", "command": "āŽ•āŽŸā¯āŽŸāŽŗā¯ˆ", + "command_palette_prompt": "āŽĒāŽ•ā¯āŽ•āŽ™ā¯āŽ•āŽŗā¯, āŽšā¯†āŽ¯āŽ˛ā¯āŽ•āŽŗā¯ āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽ•āŽŸā¯āŽŸāŽŗā¯ˆāŽ•āŽŗā¯ˆ āŽĩāŽŋāŽ°ā¯ˆāŽĩāŽžāŽ•āŽ•ā¯ āŽ•āŽŖā¯āŽŸāŽąāŽŋāŽ¯āŽĩā¯āŽŽā¯", + "command_palette_to_close": "āŽŽā¯‚āŽŸā¯āŽĩāŽ¤āŽąā¯āŽ•ā¯", + "command_palette_to_navigate": "āŽ¨ā¯āŽ´ā¯ˆāŽ¯", + "command_palette_to_select": "āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•", + "command_palette_to_show_all": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ˆāŽ¯ā¯āŽŽā¯ āŽ•āŽžāŽŸā¯āŽŸ", "comment_deleted": "āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤ā¯ āŽ¨ā¯€āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "comment_options": "āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤ā¯ āŽĩāŽŋāŽ°ā¯āŽĒā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯", "comments_and_likes": "āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤ā¯āŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽĩāŽŋāŽ°ā¯āŽĒā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯", @@ -790,6 +843,7 @@ "create_album": "āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤ā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "create_album_page_untitled": "āŽ¤āŽ˛ā¯ˆāŽĒā¯āŽĒāŽŋāŽŸāŽĒā¯āŽĒāŽŸāŽžāŽ¤", "create_api_key": "āŽĒāŽ¨āŽŋāŽ‡ āŽĩāŽŋāŽšā¯ˆāŽ¯ā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "create_first_workflow": "āŽŽā¯āŽ¤āŽ˛ā¯ āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩ❁ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "create_library": "āŽ¨ā¯‚āŽ˛āŽ•āŽ¤ā¯āŽ¤ā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "create_link": "āŽ‡āŽŖā¯ˆāŽĒā¯āŽĒ❈ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "create_link_to_share": "āŽĒāŽ•āŽŋāŽ°ā¯āŽĩā¯āŽ•ā¯āŽ•ā¯ āŽ‡āŽŖā¯ˆāŽĒā¯āŽĒ❈ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -804,17 +858,25 @@ "create_tag": "āŽ•ā¯āŽąāŽŋāŽšā¯āŽšā¯ŠāŽ˛ā¯āŽ˛ā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "create_tag_description": "āŽĒā¯āŽ¤āŽŋāŽ¯ āŽ•ā¯āŽąāŽŋāŽšā¯āŽšā¯ŠāŽ˛ā¯āŽ˛ā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯. āŽ‰āŽŗā¯āŽŗāŽŽā¯ˆāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ•ā¯āŽąāŽŋāŽšā¯āŽšā¯ŠāŽąā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯, āŽŽā¯āŽŠā¯āŽŠā¯‹āŽ•ā¯āŽ•āŽŋ āŽšā¯āŽ˛āŽžāŽšā¯āŽ•āŽŗā¯ āŽ‰āŽŸā¯āŽĒāŽŸ āŽ•ā¯āŽąāŽŋāŽšā¯āŽšā¯ŠāŽ˛ā¯āŽ˛āŽŋāŽŠā¯ āŽŽā¯āŽ´ā¯ āŽĒāŽžāŽ¤ā¯ˆāŽ¯ā¯ˆāŽ¯ā¯āŽŽā¯ āŽ‰āŽŗā¯āŽŗāŽŋāŽŸāŽĩā¯āŽŽā¯.", "create_user": "āŽĒāŽ¯āŽŠāŽ°ā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•ā¯", + "create_workflow": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩ❁ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "created": "āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "created_at": "āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "creating_linked_albums": "āŽ‡āŽŖā¯ˆāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•ā¯āŽ¤āŽ˛ā¯ ...", "crop": "āŽĒāŽ¯āŽŋāŽ°ā¯", + "crop_aspect_ratio_fixed": "āŽšāŽ°āŽŋ āŽšā¯†āŽ¯ā¯āŽ¯āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", + "crop_aspect_ratio_free": "āŽ‡āŽ˛āŽĩāŽšāŽŽā¯", + "crop_aspect_ratio_original": "āŽ…āŽšāŽ˛ā¯", "curated_object_page_title": "āŽĩāŽŋāŽšāŽ¯āŽ™ā¯āŽ•āŽŗā¯", "current_device": "āŽ¤āŽąā¯āŽĒā¯‹āŽ¤ā¯ˆāŽ¯ āŽšāŽžāŽ¤āŽŠāŽŽā¯", "current_pin_code": "āŽ¤āŽąā¯āŽĒā¯‹āŽ¤ā¯ˆāŽ¯ āŽŽā¯āŽŗā¯ āŽ•ā¯āŽąāŽŋāŽ¯ā¯€āŽŸā¯", "current_server_address": "āŽ¤āŽąā¯āŽĒā¯‹āŽ¤ā¯ˆāŽ¯ āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ• āŽŽā¯āŽ•āŽĩāŽ°āŽŋ", - "custom_locale": "āŽ¤āŽŠāŽŋāŽĒā¯āŽĒāŽ¯āŽŠā¯ āŽ‡āŽŸāŽŽā¯", - "custom_locale_description": "āŽŽā¯ŠāŽ´āŽŋ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽĒāŽŋāŽ°āŽžāŽ¨ā¯āŽ¤āŽŋāŽ¯āŽ¤ā¯āŽ¤āŽŋāŽŠā¯ āŽ…āŽŸāŽŋāŽĒā¯āŽĒāŽŸā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽĩāŽŸāŽŋāŽĩāŽŽā¯ˆāŽĒā¯āŽĒ❁ āŽ¤ā¯‡āŽ¤āŽŋāŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽŽāŽŖā¯āŽ•āŽŗā¯", + "custom_date": "āŽĩāŽŋāŽ°ā¯āŽĒā¯āŽĒ āŽ¤ā¯‡āŽ¤āŽŋ", + "custom_locale": "āŽ¤āŽŠāŽŋāŽĒā¯āŽĒāŽ¯āŽŠā¯ āŽŽā¯ŠāŽ´āŽŋ", + "custom_locale_description": "āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽŽā¯ŠāŽ´āŽŋ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽĒāŽŋāŽ°āŽžāŽ¨ā¯āŽ¤āŽŋāŽ¯āŽ¤ā¯āŽ¤āŽŋāŽŠā¯ āŽ…āŽŸāŽŋāŽĒā¯āŽĒāŽŸā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽ¤ā¯‡āŽ¤āŽŋāŽ•āŽŗā¯, āŽ¨ā¯‡āŽ°āŽŽā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽŽāŽŖā¯āŽ•āŽŗā¯ˆ āŽĩāŽŸāŽŋāŽĩāŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "custom_url": "āŽ¤āŽŠāŽŋāŽĒā¯āŽĒāŽ¯āŽŠā¯ āŽŽā¯āŽ•āŽĩāŽ°āŽŋ", + "cutoff_date_description": "āŽ•āŽŸā¯ˆāŽšāŽŋ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ˆ āŽĩā¯ˆāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ™ā¯āŽ•āŽŗā¯â€Ļ", + "cutoff_day": "{count, plural, one {āŽ¨āŽžāŽŗā¯} other {āŽ¨āŽžāŽŗā¯āŽ•āŽŗā¯}}", + "cutoff_year": "{count, plural, one {āŽ†āŽŖā¯āŽŸā¯} other {āŽ†āŽŖā¯āŽŸā¯āŽ•āŽŗā¯}}", "daily_title_text_date": "E, mmm dd", "daily_title_text_date_year": "E, mmm dd, yyyy", "dark": "āŽ‡āŽ°ā¯āŽŖā¯āŽŸ", @@ -829,10 +891,6 @@ "day": "āŽ¨āŽžāŽŗā¯", "days": "āŽ¨āŽžāŽŸā¯āŽ•āŽŗā¯", "deduplicate_all": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ˆāŽ¯ā¯āŽŽā¯ āŽ•āŽ´āŽŋāŽ¤ā¯āŽ¤āŽ˛ā¯", - "deduplication_criteria_1": "āŽĒā¯ˆāŽŸā¯āŽŸā¯āŽ•āŽŗāŽŋāŽ˛ā¯ āŽĒāŽŸ āŽ…āŽŗāŽĩ❁", - "deduplication_criteria_2": "EXIF āŽ¤āŽ°āŽĩāŽŋāŽŠā¯ āŽŽāŽŖā¯āŽŖāŽŋāŽ•ā¯āŽ•ā¯ˆ", - "deduplication_info": "āŽ•āŽ´āŽŋāŽ¤ā¯āŽ¤āŽ˛ā¯ āŽšā¯†āŽ¯ā¯āŽ¤āŽŋ", - "deduplication_info_description": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ˆ āŽ¤āŽžāŽŠāŽžāŽ• āŽŽā¯āŽŠā¯āŽŠā¯†āŽŸā¯āŽ¤ā¯āŽ¤ā¯āŽšā¯ āŽšā¯†āŽ˛ā¯āŽ˛āŽĩā¯āŽŽā¯, āŽŽā¯ŠāŽ¤ā¯āŽ¤āŽŽāŽžāŽ• āŽ¨āŽ•āŽ˛ā¯āŽ•āŽŗā¯ˆ āŽ…āŽ•āŽąā¯āŽąāŽĩā¯āŽŽā¯, āŽ¨āŽžāŽ™ā¯āŽ•āŽŗā¯ āŽĒāŽžāŽ°ā¯āŽ•ā¯āŽ•āŽŋāŽąā¯‹āŽŽā¯:", "delete": "āŽ¨ā¯€āŽ•ā¯āŽ•ā¯", "delete_action_confirmation_message": "āŽ‡āŽ¨ā¯āŽ¤ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ˆ āŽ¨ā¯€āŽ•ā¯āŽ• āŽĩāŽŋāŽ°ā¯āŽŽā¯āŽĒā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗāŽž? āŽ‡āŽ¨ā¯āŽ¤ āŽ¨āŽŸāŽĩāŽŸāŽŋāŽ•ā¯āŽ•ā¯ˆ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ˆ āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ•āŽ¤ā¯āŽ¤āŽŋāŽŠā¯ āŽ•ā¯āŽĒā¯āŽĒā¯ˆāŽ•ā¯āŽ•ā¯ āŽ¨āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤ā¯āŽŽā¯, āŽŽā¯‡āŽ˛ā¯āŽŽā¯ āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽ…āŽ¤ā¯ˆ āŽ‰āŽŗā¯āŽ¨āŽžāŽŸā¯āŽŸāŽŋāŽ˛ā¯ āŽ¨ā¯€āŽ•ā¯āŽ• āŽĩāŽŋāŽ°ā¯āŽŽā¯āŽĒāŽŋāŽŠāŽžāŽ˛ā¯ āŽ•ā¯‡āŽŸā¯āŽ•ā¯āŽŽā¯", "delete_action_prompt": "{count} āŽ¨ā¯€āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", @@ -868,6 +926,7 @@ "deselect_all": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ˆāŽ¯ā¯āŽŽā¯ āŽ¤ā¯‡āŽ°ā¯āŽĩ❁ āŽšā¯†āŽ¯ā¯āŽ¯ā¯āŽ™ā¯āŽ•āŽŗā¯", "details": "āŽĩāŽŋāŽĩāŽ°āŽ™ā¯āŽ•āŽŗā¯", "direction": "āŽ¤āŽŋāŽšā¯ˆ", + "disable": "āŽŽā¯āŽŸāŽ•ā¯āŽ•ā¯", "disabled": "āŽŽā¯āŽŸāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "disallow_edits": "āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤āŽ™ā¯āŽ•āŽŗā¯ˆ āŽ…āŽŠā¯āŽŽāŽ¤āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "discord": "āŽŽā¯āŽ°āŽŖā¯āŽĒāŽžāŽŸā¯", @@ -893,6 +952,7 @@ "download_include_embedded_motion_videos": "āŽ‰āŽŸā¯āŽĒā¯ŠāŽ¤āŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯", "download_include_embedded_motion_videos_description": "āŽŽā¯‹āŽšāŽŠā¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗāŽŋāŽ˛ā¯ āŽ‰āŽŸā¯āŽĒā¯ŠāŽ¤āŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ˆ āŽ¤āŽŠāŽŋ āŽ•ā¯‹āŽĒā¯āŽĒāŽžāŽ• āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "download_notfound": "āŽĒāŽ¤āŽŋāŽĩāŽŋāŽąāŽ•ā¯āŽ•āŽŽā¯ āŽ•āŽŋāŽŸā¯ˆāŽ•ā¯āŽ•āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", + "download_original": "āŽ…āŽšāŽ˛ā¯ āŽĒāŽ¤āŽŋāŽĩāŽŋāŽąāŽ•ā¯āŽ•", "download_paused": "āŽ‡āŽŸā¯ˆāŽ¨āŽŋāŽąā¯āŽ¤ā¯āŽ¤āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "download_settings": "āŽĒāŽ¤āŽŋāŽĩāŽŋāŽąāŽ•ā¯āŽ•āŽŽā¯", "download_settings_description": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽĒāŽ¤āŽŋāŽĩāŽŋāŽąāŽ•ā¯āŽ•āŽŽā¯ āŽ¤ā¯ŠāŽŸāŽ°ā¯āŽĒāŽžāŽŠ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -902,6 +962,7 @@ "download_waiting_to_retry": "āŽŽā¯€āŽŖā¯āŽŸā¯āŽŽā¯ āŽŽā¯āŽ¯āŽąā¯āŽšāŽŋāŽ•ā¯āŽ• āŽ•āŽžāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯", "downloading": "āŽĒāŽ¤āŽŋāŽĩāŽŋāŽąāŽ•ā¯āŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯", "downloading_asset_filename": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽĒāŽ¤āŽŋāŽĩāŽŋāŽąāŽ•ā¯āŽ•āŽŽā¯ {filename}", + "downloading_from_icloud": "iCloud āŽ‡āŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽĒāŽ¤āŽŋāŽĩāŽŋāŽąāŽ•ā¯āŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯", "downloading_media": "āŽŠāŽŸāŽ•āŽ™ā¯āŽ•āŽŗā¯ˆāŽĒā¯ āŽĒāŽ¤āŽŋāŽĩāŽŋāŽąāŽ•ā¯āŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯", "drop_files_to_upload": "āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽą āŽŽāŽ™ā¯āŽ•ā¯āŽŽā¯ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽĩāŽŋāŽŸā¯āŽ™ā¯āŽ•āŽŗā¯", "duplicates": "āŽ¨āŽ•āŽ˛ā¯āŽ•āŽŗā¯", @@ -930,9 +991,24 @@ "edit_tag": "āŽ•ā¯āŽąāŽŋāŽšā¯āŽšā¯ŠāŽ˛ā¯āŽ˛ā¯ˆāŽ¤ā¯ āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤ā¯", "edit_title": "āŽ¤āŽ˛ā¯ˆāŽĒā¯āŽĒā¯ˆāŽ¤ā¯ āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤ā¯", "edit_user": "āŽĒāŽ¯āŽŠāŽ°ā¯ˆāŽ¤ā¯ āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤ā¯", + "edit_workflow": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩā¯āŽ•āŽŗā¯ˆāŽ¤ā¯ āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", "editor": "āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤āŽŋ", "editor_close_without_save_prompt": "āŽŽāŽžāŽąā¯āŽąāŽ™ā¯āŽ•āŽŗā¯ āŽšā¯‡āŽŽāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽžāŽ¤ā¯", "editor_close_without_save_title": "āŽŽā¯‚āŽŸā¯ āŽ†āŽšāŽŋāŽ°āŽŋāŽ¯āŽ°ā¯?", + "editor_confirm_reset_all_changes": "āŽŽāŽ˛ā¯āŽ˛āŽž āŽŽāŽžāŽąā¯āŽąāŽ™ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆāŽ•ā¯āŽ• āŽĩāŽŋāŽ°ā¯āŽŽā¯āŽĒā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗāŽž?", + "editor_discard_edits_confirm": "āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤āŽ™ā¯āŽ•āŽŗā¯ˆ āŽ¨āŽŋāŽ°āŽžāŽ•āŽ°āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "editor_discard_edits_prompt": "āŽ‰āŽ™ā¯āŽ•āŽŗāŽŋāŽŸāŽŽā¯ āŽšā¯‡āŽŽāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽžāŽ¤ āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤āŽ™ā¯āŽ•āŽŗā¯ āŽ‰āŽŗā¯āŽŗāŽŠ. āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽ¨āŽŋāŽšā¯āŽšāŽ¯āŽŽāŽžāŽ• āŽ…āŽĩāŽąā¯āŽąā¯ˆ āŽ¨āŽŋāŽ°āŽžāŽ•āŽ°āŽŋāŽ•ā¯āŽ• āŽĩāŽŋāŽ°ā¯āŽŽā¯āŽĒā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗāŽž?", + "editor_discard_edits_title": "āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤āŽ™ā¯āŽ•āŽŗā¯ˆ āŽ¨āŽŋāŽ°āŽžāŽ•āŽ°āŽŋāŽ•ā¯āŽ•āŽĩāŽž?", + "editor_edits_applied_error": "āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤āŽ™ā¯āŽ•āŽŗā¯ˆāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤ā¯āŽĩāŽ¤āŽŋāŽ˛ā¯ āŽ¤ā¯‹āŽ˛ā¯āŽĩāŽŋ", + "editor_edits_applied_success": "āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤āŽ™ā¯āŽ•āŽŗā¯ āŽĩā¯†āŽąā¯āŽąāŽŋāŽ•āŽ°āŽŽāŽžāŽ•āŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŠ", + "editor_flip_horizontal": "āŽ•āŽŋāŽŸā¯ˆāŽŽāŽŸā¯āŽŸāŽŽāŽžāŽ• āŽĒā¯āŽ°āŽŸā¯āŽŸāŽĩā¯āŽŽā¯", + "editor_flip_vertical": "āŽšā¯†āŽ™ā¯āŽ•ā¯āŽ¤ā¯āŽ¤āŽžāŽ• āŽĒā¯āŽ°āŽŸā¯āŽŸāŽĩā¯āŽŽā¯", + "editor_handle_corner": "{corner, select, top_left {āŽŽā¯‡āŽ˛ā¯-āŽ‡āŽŸāŽ¤ā¯} top_right {āŽŽā¯‡āŽ˛ā¯-āŽĩāŽ˛āŽ¤ā¯} bottom_left {āŽ•ā¯€āŽ´ā¯-āŽ‡āŽŸāŽ¤ā¯} bottom_right {āŽ•ā¯€āŽ´ā¯-āŽĩāŽ˛āŽ¤ā¯} other {āŽ’āŽ°ā¯}} āŽŽā¯‚āŽ˛ā¯ˆ āŽ•ā¯ˆāŽĒā¯āŽĒāŽŋāŽŸāŽŋ", + "editor_handle_edge": "{edge, select, top {āŽŽā¯‡āŽ˛ā¯‡} bottom {āŽ•ā¯€āŽ´ā¯‡} left {āŽ‡āŽŸāŽ¤ā¯} right {āŽĩāŽ˛āŽ¤ā¯} other {āŽ’āŽ°ā¯}} āŽĩāŽŋāŽŗāŽŋāŽŽā¯āŽĒ❁ āŽ•ā¯ˆāŽĒā¯āŽĒāŽŋāŽŸāŽŋ", + "editor_orientation": "āŽ¨ā¯‹āŽ•ā¯āŽ•ā¯āŽ¨āŽŋāŽ˛ā¯ˆ", + "editor_reset_all_changes": "āŽŽāŽžāŽąā¯āŽąāŽ™ā¯āŽ•āŽŗā¯ˆ āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "editor_rotate_left": "āŽŽāŽ¤āŽŋāŽ°ā¯†āŽ¤āŽŋāŽ°ā¯ āŽ¤āŽŋāŽšā¯ˆāŽ¯āŽŋāŽ˛ā¯ 90° āŽšā¯āŽ´āŽąā¯āŽąā¯", + "editor_rotate_right": "āŽ•āŽŸāŽŋāŽ•āŽžāŽ° āŽ¤āŽŋāŽšā¯ˆāŽ¯āŽŋāŽ˛ā¯ 90° āŽšā¯āŽ´āŽąā¯āŽąā¯", "email": "āŽŽāŽŋāŽŠā¯āŽŠāŽžā¯āŽšāŽ˛ā¯", "email_notifications": "āŽŽāŽŋāŽŠā¯āŽŠāŽžā¯āŽšāŽ˛ā¯ āŽ…āŽąāŽŋāŽĩāŽŋāŽĒā¯āŽĒā¯āŽ•āŽŗā¯", "empty_folder": "āŽ‡āŽ¨ā¯āŽ¤ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽąā¯ˆ āŽ•āŽžāŽ˛āŽŋāŽ¯āŽžāŽ• āŽ‰āŽŗā¯āŽŗāŽ¤ā¯", @@ -951,11 +1027,14 @@ "error_change_sort_album": "āŽ†āŽ˛ā¯āŽĒāŽŽā¯ āŽĩāŽ°āŽŋāŽšā¯ˆ āŽĩāŽ°āŽŋāŽšā¯ˆāŽ¯ā¯ˆ āŽŽāŽžāŽąā¯āŽąāŽ¤ā¯ āŽ¤āŽĩāŽąāŽŋāŽĩāŽŋāŽŸā¯āŽŸāŽ¤ā¯", "error_delete_face": "āŽšā¯ŠāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽ‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽŽā¯āŽ•āŽ¤ā¯āŽ¤ā¯ˆ āŽ¨ā¯€āŽ•ā¯āŽ•ā¯āŽĩāŽ¤āŽŋāŽ˛ā¯ āŽĒāŽŋāŽ´ā¯ˆ", "error_getting_places": "āŽ‡āŽŸāŽ™ā¯āŽ•āŽŗā¯ˆāŽĒā¯ āŽĒā¯†āŽąā¯āŽĩāŽ¤āŽŋāŽ˛ā¯ āŽĒāŽŋāŽ´ā¯ˆ", + "error_loading_albums": "āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ˆ āŽāŽąā¯āŽąā¯āŽĩāŽ¤āŽŋāŽ˛ā¯ āŽĒāŽŋāŽ´ā¯ˆ", "error_loading_image": "āŽĒāŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽāŽąā¯āŽąā¯āŽĩāŽ¤āŽŋāŽ˛ā¯ āŽĒāŽŋāŽ´ā¯ˆ", "error_loading_partners": "āŽ•ā¯‚āŽŸā¯āŽŸāŽžāŽŗāŽ°ā¯āŽ•āŽŗā¯ˆ āŽāŽąā¯āŽąā¯āŽĩāŽ¤āŽŋāŽ˛ā¯ āŽĒāŽŋāŽ´ā¯ˆ: {error}", + "error_retrieving_asset_information": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽ¤āŽ•āŽĩāŽ˛ā¯ˆ āŽŽā¯€āŽŸā¯āŽŸā¯†āŽŸā¯āŽĒā¯āŽĒāŽ¤āŽŋāŽ˛ā¯ āŽĒāŽŋāŽ´ā¯ˆ", "error_saving_image": "āŽĒāŽŋāŽ´ā¯ˆ: {error}", "error_tag_face_bounding_box": "āŽŽā¯āŽ•āŽ¤ā¯āŽ¤ā¯ˆ āŽ•ā¯āŽąāŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯ āŽĒāŽŋāŽ´ā¯ˆ - āŽŽāŽ˛ā¯āŽ˛ā¯ˆ āŽĒā¯†āŽŸā¯āŽŸāŽŋ āŽ†āŽ¯āŽ¤ā¯āŽ¤ā¯ŠāŽ˛ā¯ˆāŽĩā¯āŽ•āŽŗā¯ˆāŽĒā¯ āŽĒā¯†āŽą āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽ¤ā¯", "error_title": "āŽĒāŽŋāŽ´ā¯ˆ - āŽāŽ¤ā¯‹ āŽ¤āŽĩāŽąā¯ āŽ¨āŽŸāŽ¨ā¯āŽ¤āŽ¤ā¯", + "error_while_navigating": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•ā¯ āŽšā¯†āŽ˛ā¯āŽ˛ā¯āŽŽā¯āŽĒā¯‹āŽ¤ā¯ āŽĒāŽŋāŽ´ā¯ˆ", "errors": { "cannot_navigate_next_asset": "āŽ…āŽŸā¯āŽ¤ā¯āŽ¤ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•ā¯ āŽšā¯†āŽ˛ā¯āŽ˛ āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽ¤ā¯", "cannot_navigate_previous_asset": "āŽŽā¯āŽ¨ā¯āŽ¤ā¯ˆāŽ¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•ā¯ āŽšā¯†āŽ˛ā¯āŽ˛ āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽ¤ā¯", @@ -991,6 +1070,7 @@ "failed_to_update_notification_status": "āŽ…āŽąāŽŋāŽĩāŽŋāŽĒā¯āŽĒ❁ āŽ¨āŽŋāŽ˛ā¯ˆāŽ¯ā¯ˆāŽĒā¯ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ•āŽ¤ā¯ āŽ¤āŽĩāŽąāŽŋāŽĩāŽŋāŽŸā¯āŽŸāŽ¤ā¯", "incorrect_email_or_password": "āŽ¤āŽĩāŽąāŽžāŽŠ āŽŽāŽŋāŽŠā¯āŽŠāŽžā¯āŽšāŽ˛ā¯ āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽ•āŽŸāŽĩā¯āŽšā¯āŽšā¯ŠāŽ˛ā¯", "library_folder_already_exists": "āŽ‡āŽ¨ā¯āŽ¤ āŽ‡āŽąāŽ•ā¯āŽ•ā¯āŽŽāŽ¤āŽŋ āŽĒāŽžāŽ¤ā¯ˆ āŽāŽąā¯āŽ•āŽŠāŽĩ❇ āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯āŽŸāŽŋāŽ˛ā¯ āŽ‰āŽŗā¯āŽŗāŽ¤ā¯.", + "page_not_found": "āŽĒāŽ•ā¯āŽ•āŽŽā¯ āŽ•āŽŋāŽŸā¯ˆāŽ•ā¯āŽ•āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "paths_validation_failed": "āŽ¤ā¯‹āŽ˛ā¯āŽĩāŽŋāŽ¯ā¯āŽąā¯āŽą āŽšāŽ°āŽŋāŽĒāŽžāŽ°ā¯āŽĒā¯āŽĒ❁ {paths, plural, one {# āŽĒāŽžāŽ¤ā¯ˆ} other {# āŽĒāŽžāŽ¤ā¯ˆāŽ•āŽŗā¯}}", "profile_picture_transparent_pixels": "āŽšā¯āŽ¯āŽĩāŽŋāŽĩāŽ°āŽĒā¯ āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯ āŽĩā¯†āŽŗāŽŋāŽĒā¯āŽĒāŽŸā¯ˆāŽ¯āŽžāŽŠ āŽĒāŽŸāŽĒā¯āŽĒā¯āŽŗā¯āŽŗāŽŋāŽ•āŽŗā¯ āŽ‡āŽ°ā¯āŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽ¤ā¯. āŽ¤āŽ¯āŽĩā¯āŽšā¯†āŽ¯ā¯āŽ¤ā¯ āŽĒā¯†āŽ°āŽŋāŽ¤āŽžāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯/āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽĒāŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽ¨āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯.", "quota_higher_than_disk_size": "āŽĩāŽŸā¯āŽŸā¯ āŽ…āŽŗāŽĩ❈ āŽĩāŽŋāŽŸ āŽ…āŽ¤āŽŋāŽ•āŽŽāŽžāŽ• āŽ’āŽ¤ā¯āŽ•ā¯āŽ•ā¯€āŽŸā¯āŽŸā¯ˆ āŽ…āŽŽā¯ˆāŽ¤ā¯āŽ¤ā¯āŽŗā¯āŽŗā¯€āŽ°ā¯āŽ•āŽŗā¯", @@ -1012,7 +1092,8 @@ "unable_to_change_visibility": "{count, plural, one {# āŽ¨āŽĒāŽ°ā¯} other {# āŽĒā¯‡āŽ°ā¯}}āŽ•ā¯āŽ•āŽžāŽŠ āŽ¤ā¯†āŽ°āŽŋāŽĩā¯āŽ¨āŽŋāŽ˛ā¯ˆāŽ¯ā¯ˆ āŽŽāŽžāŽąā¯āŽą āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_complete_oauth_login": "OAuth āŽ‰āŽŗā¯āŽ¨ā¯āŽ´ā¯ˆāŽĩ❈ āŽŽā¯āŽŸāŽŋāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_connect": "āŽ‡āŽŖā¯ˆāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", - "unable_to_copy_to_clipboard": "āŽ‡āŽŸā¯ˆāŽ¨āŽŋāŽ˛ā¯ˆāŽĒā¯āŽĒāŽ˛āŽ•ā¯ˆāŽ•ā¯āŽ•ā¯ āŽ¨āŽ•āŽ˛ā¯†āŽŸā¯āŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽ¤ā¯, āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ HTTPS āŽŽā¯‚āŽ˛āŽŽā¯ āŽĒāŽ•ā¯āŽ•āŽ¤ā¯āŽ¤ā¯ˆ āŽ…āŽŖā¯āŽ•ā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗā¯ āŽŽāŽŠā¯āŽĒāŽ¤ā¯ˆ āŽ‰āŽąā¯āŽ¤āŽŋāŽĒā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽŋāŽ•ā¯ āŽ•ā¯ŠāŽŗā¯āŽŗā¯āŽ™ā¯āŽ•āŽŗā¯", + "unable_to_copy_to_clipboard": "āŽ‡āŽŸā¯ˆāŽ¨āŽŋāŽ˛ā¯ˆāŽĒā¯āŽĒāŽ˛āŽ•ā¯ˆāŽ•ā¯āŽ•ā¯ āŽ¨āŽ•āŽ˛ā¯†āŽŸā¯āŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽ¤ā¯, āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽ‰āŽ‰āŽĒāŽ¨ā¯†āŽĒ āŽŽā¯‚āŽ˛āŽŽā¯ āŽĒāŽ•ā¯āŽ•āŽ¤ā¯āŽ¤ā¯ˆ āŽ…āŽŖā¯āŽ•ā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗā¯ āŽŽāŽŠā¯āŽĒāŽ¤ā¯ˆ āŽ‰āŽąā¯āŽ¤āŽŋāŽĒā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽŋāŽ•ā¯ āŽ•ā¯ŠāŽŗā¯āŽŗā¯āŽ™ā¯āŽ•āŽŗā¯", + "unable_to_create": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩā¯āŽ•āŽŗā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_create_admin_account": "āŽ¨āŽŋāŽ°ā¯āŽĩāŽžāŽ• āŽ•āŽŖāŽ•ā¯āŽ•ā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_create_api_key": "āŽĒā¯āŽ¤āŽŋāŽ¯ āŽĒāŽ¨āŽŋāŽ‡ āŽĩāŽŋāŽšā¯ˆāŽ¯ā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_create_library": "āŽ¨ā¯‚āŽ˛āŽ•āŽ¤ā¯āŽ¤ā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", @@ -1023,6 +1104,7 @@ "unable_to_delete_exclusion_pattern": "āŽĩāŽŋāŽ˛āŽ•ā¯āŽ•ā¯ āŽŽā¯āŽąā¯ˆāŽ¯ā¯ˆ āŽ¨ā¯€āŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_delete_shared_link": "āŽĒāŽ•āŽŋāŽ°āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ‡āŽŖā¯ˆāŽĒā¯āŽĒ❈ āŽ¨ā¯€āŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_delete_user": "āŽĒāŽ¯āŽŠāŽ°ā¯ˆ āŽ¨ā¯€āŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", + "unable_to_delete_workflow": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩā¯āŽ•āŽŗā¯ˆ āŽ¨ā¯€āŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_download_files": "āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆāŽĒā¯ āŽĒāŽ¤āŽŋāŽĩāŽŋāŽąāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_edit_exclusion_pattern": "āŽĩāŽŋāŽ˛āŽ•ā¯āŽ•ā¯ āŽŽā¯āŽąā¯ˆāŽ¯ā¯ˆāŽ¤ā¯ āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤ āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_empty_trash": "āŽ•ā¯āŽĒā¯āŽĒā¯ˆāŽ•āŽŗā¯ˆ āŽĩā¯†āŽąā¯āŽąā¯ āŽšā¯†āŽ¯ā¯āŽ¯ āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", @@ -1062,6 +1144,7 @@ "unable_to_scan_library": "āŽ¨ā¯‚āŽ˛āŽ•āŽ¤ā¯āŽ¤ā¯ˆ āŽšā¯āŽ•ā¯‡āŽŠā¯ āŽšā¯†āŽ¯ā¯āŽ¯ āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_set_feature_photo": "āŽ…āŽŽā¯āŽš āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽ…āŽŽā¯ˆāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_set_profile_picture": "āŽšā¯āŽ¯āŽĩāŽŋāŽĩāŽ°āŽĒā¯ āŽĒāŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽ…āŽŽā¯ˆāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", + "unable_to_set_rating": "āŽŽāŽ¤āŽŋāŽĒā¯āŽĒā¯€āŽŸā¯āŽŸā¯ˆ āŽ…āŽŽā¯ˆāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_submit_job": "āŽĩā¯‡āŽ˛ā¯ˆāŽ¯ā¯ˆāŽšā¯ āŽšāŽŽāŽ°ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_trash_asset": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ˆ āŽ•ā¯āŽĒā¯āŽĒ❈ āŽšā¯†āŽ¯ā¯āŽ¯ āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_unlink_account": "āŽ•āŽŖāŽ•ā¯āŽ•ā¯ˆ āŽ‡āŽŖā¯ˆāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", @@ -1073,8 +1156,10 @@ "unable_to_update_settings": "āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_update_timeline_display_status": "āŽ•āŽžāŽ˛āŽĩāŽ°āŽŋāŽšā¯ˆ āŽ•āŽžāŽŸā¯āŽšāŽŋ āŽ¨āŽŋāŽ˛ā¯ˆāŽ¯ā¯ˆ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_update_user": "āŽĒāŽ¯āŽŠāŽ°ā¯ˆāŽĒā¯ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", + "unable_to_update_workflow": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩā¯āŽ•āŽŗā¯ˆāŽĒā¯ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_upload_file": "āŽ•ā¯‹āŽĒā¯āŽĒ❈āŽĒā¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽą āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ" }, + "errors_text": "āŽĒāŽŋāŽ´ā¯ˆāŽ•āŽŗā¯", "exclusion_pattern": "āŽĩāŽŋāŽ˛āŽ•ā¯āŽ•ā¯ āŽŽā¯āŽąā¯ˆ", "exif": "āŽŽāŽ•ā¯āŽ¸āŽŋāŽƒāŽĒā¯", "exif_bottom_sheet_description": "āŽĩāŽŋāŽŗāŽ•ā¯āŽ•āŽ¤ā¯āŽ¤ā¯ˆāŽšā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯ ...", @@ -1085,6 +1170,7 @@ "exif_bottom_sheet_people": "āŽŽāŽ•ā¯āŽ•āŽŗā¯", "exif_bottom_sheet_person_add_person": "āŽĒā¯†āŽ¯āŽ°ā¯ˆāŽšā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "exit_slideshow": "āŽšā¯āŽ˛ā¯ˆāŽŸā¯āŽšā¯‹āŽĩāŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽĩā¯†āŽŗāŽŋāŽ¯ā¯‡āŽąāŽĩā¯āŽŽā¯", + "expand": "āŽĩāŽŋāŽ°āŽŋāŽĩāŽžāŽ•ā¯āŽ•ā¯", "expand_all": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ˆāŽ¯ā¯āŽŽā¯ āŽĩāŽŋāŽ°āŽŋāŽĩāŽžāŽ•ā¯āŽ•ā¯āŽ™ā¯āŽ•āŽŗā¯", "experimental_settings_new_asset_list_subtitle": "āŽĩā¯‡āŽ˛ā¯ˆ āŽŽā¯āŽŠā¯āŽŠā¯‡āŽąā¯āŽąāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽ‰āŽŗā¯āŽŗāŽ¤ā¯", "experimental_settings_new_asset_list_title": "āŽšā¯‹āŽ¤āŽŠā¯ˆ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸ āŽ•āŽŸā¯āŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -1106,6 +1192,7 @@ "external_network_sheet_info": "āŽĩāŽŋāŽ°ā¯āŽĒā¯āŽĒāŽŽāŽžāŽŠ āŽĩā¯ˆāŽƒāŽĒ❈ āŽ¨ā¯†āŽŸā¯āŽĩā¯ŠāŽ°ā¯āŽ•ā¯āŽ•āŽŋāŽ˛ā¯ āŽ‡āŽ˛ā¯āŽ˛āŽžāŽ¤āŽĒā¯‹āŽ¤ā¯, āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯ āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ•āŽ¤ā¯āŽ¤ā¯āŽŸāŽŠā¯ āŽ•ā¯€āŽ´ā¯‡ āŽ‰āŽŗā¯āŽŗ āŽŽā¯āŽ•āŽĩāŽ°āŽŋ āŽ•āŽŗāŽŋāŽŠā¯ āŽŽā¯āŽ¤āŽ˛ā¯ āŽĩāŽ´āŽŋāŽ¯āŽžāŽ• āŽ‡āŽŖā¯ˆāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯, āŽ‡āŽ¤ā¯ āŽŽā¯‡āŽ˛ā¯‡ āŽ‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽ•ā¯€āŽ´ā¯‡ āŽ¤ā¯ŠāŽŸāŽ™ā¯āŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯", "face_unassigned": "āŽ’āŽ¤ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽžāŽ¤āŽ¤ā¯", "failed": "āŽ¤ā¯‹āŽ˛ā¯āŽĩāŽŋāŽ¯ā¯āŽąā¯āŽąāŽ¤ā¯", + "failed_count": "āŽ¤ā¯‹āŽ˛ā¯āŽĩāŽŋ: {count}", "failed_to_authenticate": "āŽ…āŽ™ā¯āŽ•ā¯€āŽ•āŽ°āŽŋāŽ•ā¯āŽ•āŽ¤ā¯ āŽ¤āŽĩāŽąāŽŋāŽĩāŽŋāŽŸā¯āŽŸāŽ¤ā¯", "failed_to_load_assets": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ˆ āŽāŽąā¯āŽąā¯āŽĩāŽ¤āŽŋāŽ˛ā¯ āŽ¤ā¯‹āŽ˛ā¯āŽĩāŽŋ", "failed_to_load_folder": "āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽąā¯ˆāŽ¯ā¯ˆ āŽāŽąā¯āŽąā¯āŽĩāŽ¤āŽŋāŽ˛ā¯ āŽ¤ā¯‹āŽ˛ā¯āŽĩāŽŋ", @@ -1119,12 +1206,17 @@ "features_in_development": "āŽĩāŽŗāŽ°ā¯āŽšā¯āŽšāŽŋāŽ¯āŽŋāŽ˛ā¯ āŽ¨āŽąā¯āŽĒā¯ŠāŽ°ā¯āŽ¤ā¯āŽ¤āŽ™ā¯āŽ•āŽŗā¯", "features_setting_description": "āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯āŽŸā¯ āŽ…āŽŽā¯āŽšāŽ™ā¯āŽ•āŽŗā¯ˆ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "file_name_or_extension": "āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽĒā¯†āŽ¯āŽ°ā¯ āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽ¨ā¯€āŽŸā¯āŽŸāŽŋāŽĒā¯āŽĒ❁", + "file_name_text": "āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽĒā¯†āŽ¯āŽ°ā¯", + "file_name_with_value": "āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽĒā¯†āŽ¯āŽ°ā¯: {file_name}", "file_size": "āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽ…āŽŗāŽĩ❁", "filename": "āŽ•ā¯‹āŽĒā¯āŽĒ❁āŽĒā¯āŽĒā¯†āŽ¯āŽ°ā¯", "filetype": "āŽĒā¯ˆāŽ˛ā¯āŽŸā¯ˆāŽĒā¯", "filter": "āŽĩāŽŸāŽŋāŽĒā¯āŽĒāŽŋ", + "filter_description": "āŽ‡āŽ˛āŽ•ā¯āŽ•ā¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ˆ āŽĩāŽŸāŽŋāŽ•āŽŸā¯āŽŸā¯āŽĩāŽ¤āŽąā¯āŽ•āŽžāŽŠ āŽ¨āŽŋāŽĒāŽ¨ā¯āŽ¤āŽŠā¯ˆāŽ•āŽŗā¯", "filter_people": "āŽŽāŽ•ā¯āŽ•āŽŗā¯ˆ āŽĩāŽŸāŽŋāŽ•āŽŸā¯āŽŸāŽĩā¯āŽŽā¯", "filter_places": "āŽ‡āŽŸāŽ™ā¯āŽ•āŽŗā¯ˆ āŽĩāŽŸāŽŋāŽ•āŽŸā¯āŽŸāŽĩā¯āŽŽā¯", + "filter_tags": "āŽ•ā¯āŽąāŽŋāŽšā¯āŽšā¯ŠāŽąā¯āŽ•āŽŗā¯ˆ āŽĩāŽŸāŽŋāŽ•āŽŸā¯āŽŸāŽŋ", + "filters": "āŽĩāŽŸāŽŋāŽĒā¯āŽĒāŽžāŽŠā¯āŽ•āŽŗā¯", "find_them_fast": "āŽ¤ā¯‡āŽŸāŽ˛ā¯āŽŸāŽŠā¯ āŽĒā¯†āŽ¯āŽ°āŽžāŽ˛ā¯ āŽĩā¯‡āŽ•āŽŽāŽžāŽ• āŽ…āŽĩāŽąā¯āŽąā¯ˆāŽ•ā¯ āŽ•āŽŖā¯āŽŸāŽąāŽŋāŽ¯āŽĩā¯āŽŽā¯", "first": "āŽŽā¯āŽ¤āŽ˛ā¯", "fix_incorrect_match": "āŽ¤āŽĩāŽąāŽžāŽŠ āŽĒā¯‹āŽŸā¯āŽŸāŽŋāŽ¯ā¯ˆ āŽšāŽ°āŽŋāŽšā¯†āŽ¯ā¯āŽ¯āŽĩā¯āŽŽā¯", @@ -1134,12 +1226,16 @@ "folders_feature_description": "āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽŽā¯āŽąā¯ˆāŽŽā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽ‰āŽŗā¯āŽŗ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•āŽžāŽŠ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽąā¯ˆ āŽ•āŽžāŽŸā¯āŽšāŽŋāŽ¯ā¯ˆ āŽ‰āŽ˛āŽžāŽĩā¯āŽ¤āŽ˛ā¯", "forgot_pin_code_question": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽŽā¯āŽŗā¯ āŽŽāŽąāŽ¨ā¯āŽ¤ā¯āŽĩāŽŋāŽŸā¯āŽŸā¯€āŽ°ā¯āŽ•āŽŗāŽž?", "forward": "āŽŽā¯āŽŠā¯āŽŠā¯‹āŽ•ā¯āŽ•āŽŋ", + "free_up_space": "āŽ‡āŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽĩāŽŋāŽŸā¯āŽĩāŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "free_up_space_description": "āŽšā¯‡āŽŽāŽŋāŽĒā¯āŽĒāŽŋāŽŸāŽ¤ā¯āŽ¤ā¯ˆāŽ•ā¯ āŽ•āŽžāŽ˛āŽŋāŽ¯āŽžāŽ•ā¯āŽ•, āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯ āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽŽāŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽšāŽžāŽ¤āŽŠāŽ¤ā¯āŽ¤āŽŋāŽŠā¯ āŽ•ā¯āŽĒā¯āŽĒā¯ˆāŽ•ā¯āŽ•ā¯ āŽ¨āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯. āŽšāŽ°ā¯āŽĩāŽ°āŽŋāŽ˛ā¯ āŽ‰āŽŗā¯āŽŗ āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽĒāŽŋāŽ°āŽ¤āŽŋāŽ•āŽŗā¯ āŽĒāŽžāŽ¤ā¯āŽ•āŽžāŽĒā¯āŽĒāŽžāŽ• āŽ‡āŽ°ā¯āŽ•ā¯āŽ•ā¯āŽŽā¯.", + "free_up_space_settings_subtitle": "āŽšāŽžāŽ¤āŽŠ āŽšā¯‡āŽŽāŽŋāŽĒā¯āŽĒāŽŋāŽŸāŽ¤ā¯āŽ¤ā¯ˆāŽ•ā¯ āŽ•āŽžāŽ˛āŽŋāŽ¯āŽžāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "full_path": "āŽŽā¯āŽ´ā¯ āŽĒāŽžāŽ¤ā¯ˆ: {path}", "gcast_enabled": "āŽ•ā¯‚āŽ•āŽŋāŽŗā¯ āŽ¨āŽŸāŽŋāŽ•āŽ°ā¯āŽ•āŽŗā¯", "gcast_enabled_description": "āŽ‡āŽ¨ā¯āŽ¤ āŽ¨āŽąā¯āŽĒā¯ŠāŽ°ā¯āŽ¤ā¯āŽ¤āŽŽā¯ āŽĩā¯‡āŽ˛ā¯ˆ āŽšā¯†āŽ¯ā¯āŽĩāŽ¤āŽąā¯āŽ•āŽžāŽ• Google āŽ‡āŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽĩā¯†āŽŗāŽŋāŽĒā¯āŽĒā¯āŽą āŽĩāŽŗāŽ™ā¯āŽ•āŽŗā¯ˆ āŽāŽąā¯āŽąā¯āŽ•āŽŋāŽąāŽ¤ā¯.", "general": "āŽĒā¯†āŽžāŽ¤ā¯", "geolocation_instruction_location": "āŽ…āŽ¤āŽŠā¯ āŽ‡āŽ°ā¯āŽĒā¯āŽĒāŽŋāŽŸāŽ¤ā¯āŽ¤ā¯ˆāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤ āŽšāŽŋ.āŽĒāŽŋ.āŽŽāŽšā¯ āŽ†āŽ¯āŽ¤ā¯āŽ¤ā¯ŠāŽ˛ā¯ˆāŽĩā¯āŽ•āŽŗā¯āŽŸāŽŠā¯ āŽ’āŽ°ā¯ āŽšā¯ŠāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽšā¯ŠāŽŸā¯āŽ•ā¯āŽ•ā¯ āŽšā¯†āŽ¯ā¯āŽ• āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽĩāŽ°ā¯ˆāŽĒāŽŸāŽ¤ā¯āŽ¤āŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽ¨ā¯‡āŽ°āŽŸāŽŋāŽ¯āŽžāŽ• āŽ’āŽ°ā¯ āŽ‡āŽŸāŽ¤ā¯āŽ¤ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "get_help": "āŽ‰āŽ¤āŽĩāŽŋ āŽĒā¯†āŽąā¯", + "get_people_error": "āŽŽāŽ•ā¯āŽ•āŽŗā¯ˆāŽĒā¯ āŽĒā¯†āŽąā¯āŽĩāŽ¤āŽŋāŽ˛ā¯ āŽĒāŽŋāŽ´ā¯ˆ", "get_wifiname_error": "āŽĩā¯ˆāŽƒāŽĒ❈ āŽĒā¯†āŽ¯āŽ°ā¯ˆāŽĒā¯ āŽĒā¯†āŽą āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ. āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽ¤ā¯‡āŽĩā¯ˆāŽ¯āŽžāŽŠ āŽ…āŽŠā¯āŽŽāŽ¤āŽŋāŽ•āŽŗā¯ˆ āŽĩāŽ´āŽ™ā¯āŽ•āŽŋāŽ¯ā¯āŽŗā¯āŽŗā¯€āŽ°ā¯āŽ•āŽŗā¯ āŽŽāŽŠā¯āŽĒāŽ¤ā¯ˆ āŽ‰āŽąā¯āŽ¤āŽŋāŽĒā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽŋāŽ•ā¯ āŽ•ā¯ŠāŽŗā¯āŽŗā¯āŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽĩā¯ˆāŽƒāŽĒ❈ āŽ¨ā¯†āŽŸā¯āŽĩā¯ŠāŽ°ā¯āŽ•ā¯āŽ•ā¯āŽŸāŽŠā¯ āŽ‡āŽŖā¯ˆāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸā¯āŽŗā¯āŽŗā¯€āŽ°ā¯āŽ•āŽŗā¯", "getting_started": "āŽ¤ā¯ŠāŽŸāŽ™ā¯āŽ•ā¯āŽ¤āŽ˛ā¯", "go_back": "āŽ¤āŽŋāŽ°ā¯āŽŽā¯āŽĒāŽŋāŽšā¯ āŽšā¯†āŽ˛ā¯āŽ˛ā¯āŽ™ā¯āŽ•āŽŗā¯", @@ -1165,12 +1261,14 @@ "header_settings_header_name_input": "āŽ¤āŽ˛ā¯ˆāŽĒā¯āŽĒ❁ āŽĒā¯†āŽ¯āŽ°ā¯", "header_settings_header_value_input": "āŽ¤āŽ˛ā¯ˆāŽĒā¯āŽĒ❁ āŽŽāŽ¤āŽŋāŽĒā¯āŽĒ❁", "headers_settings_tile_title": "āŽ¤āŽŠāŽŋāŽĒā¯āŽĒāŽ¯āŽŠā¯ āŽĒāŽ¤āŽŋāŽ˛āŽžāŽŗā¯ āŽ¤āŽ˛ā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯", + "height": "āŽ‰āŽ¯āŽ°āŽŽā¯", "hi_user": "āŽ†āŽ¯ā¯ {name} ({email})", "hide_all_people": "āŽŽāŽ˛ā¯āŽ˛āŽž āŽŽāŽ•ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽŽāŽąā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "hide_gallery": "āŽ•ā¯‡āŽ˛āŽ°āŽŋāŽ¯ā¯ˆ āŽŽāŽąā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "hide_named_person": "āŽ¨āŽĒāŽ°ā¯ˆ āŽŽāŽąā¯ˆāŽ•ā¯āŽ• {name}", "hide_password": "āŽ•āŽŸāŽĩā¯āŽšā¯āŽšā¯ŠāŽ˛ā¯āŽ˛ā¯ˆ āŽŽāŽąā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "hide_person": "āŽ¨āŽĒāŽ°ā¯ˆ āŽŽāŽąā¯ˆāŽ•ā¯āŽ•", + "hide_schema": "āŽ¤āŽŋāŽŸā¯āŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽŽāŽąā¯ˆ", "hide_text_recognition": "āŽ‰āŽ°ā¯ˆ āŽ…āŽ™ā¯āŽ•ā¯€āŽ•āŽžāŽ°āŽ¤ā¯āŽ¤ā¯ˆ āŽŽāŽąā¯ˆ", "hide_unnamed_people": "āŽĒā¯†āŽ¯āŽ°āŽŋāŽŸāŽĒā¯āŽĒāŽŸāŽžāŽ¤āŽĩāŽ°ā¯āŽ•āŽŗā¯ˆ āŽŽāŽąā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "home_page_add_to_album_conflicts": "{album} āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ {added} āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŠ. {failed} āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ āŽāŽąā¯āŽ•āŽŠāŽĩ❇ āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽ‰āŽŗā¯āŽŗāŽŠ.", @@ -1194,8 +1292,8 @@ "hours": "āŽŽāŽŖāŽŋ", "id": "āŽāŽŸāŽŋ", "idle": "āŽ¨āŽŋāŽ˛ā¯ˆāŽ¯āŽŋāŽ•ā¯āŽ•āŽŽā¯", - "ignore_icloud_photos": "ICloud āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ˆ āŽĒā¯āŽąāŽ•ā¯āŽ•āŽŖāŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", - "ignore_icloud_photos_description": "ICloud āŽ‡āŽ˛ā¯ āŽšā¯‡āŽŽāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯ āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ•āŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽĒā¯āŽĒāŽŸāŽžāŽ¤ā¯", + "ignore_icloud_photos": "āŽāŽŽā¯āŽ•āŽŋāŽ˛ā¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ˆāŽĒā¯ āŽĒā¯āŽąāŽ•ā¯āŽ•āŽŖāŽŋ", + "ignore_icloud_photos_description": "āŽāŽŽā¯āŽ•āŽŋāŽ˛ā¯ āŽ‡āŽ˛ā¯ āŽšā¯‡āŽŽāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯ āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ•āŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽĒā¯āŽĒāŽŸāŽžāŽ¤ā¯", "image": "āŽĒāŽŸāŽŽā¯", "image_alt_text_date": "{isVideo, select, true {āŽ•āŽžāŽŖā¯ŠāŽŗāŽŋ} other {āŽĒāŽŸāŽŽā¯}} {date} āŽ…āŽŠā¯āŽąā¯ āŽŽāŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "image_alt_text_date_1_person": "{isVideo, select, true {āŽ•āŽžāŽŖā¯ŠāŽŗāŽŋ} other {āŽĒāŽŸāŽŽā¯}} {person1} āŽ‰āŽŸāŽŠā¯ {date} āŽ…āŽŠā¯āŽąā¯ āŽŽāŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", @@ -1243,9 +1341,18 @@ "ios_debug_info_processing_ran_at": "āŽšā¯†āŽ¯āŽ˛āŽžāŽ•ā¯āŽ•āŽŽā¯ {dateTime}", "items_count": "{count, plural, one {# āŽ‰āŽ°ā¯āŽĒā¯āŽĒāŽŸāŽŋ} other {# āŽ‰āŽ°ā¯āŽĒā¯āŽĒāŽŸāŽŋāŽ•āŽŗā¯}}", "jobs": "āŽĩā¯‡āŽ˛ā¯ˆāŽ•āŽŗā¯", + "json_editor": "āŽšāŽžāŽ¤ā¯ŠāŽĒā¯ŠāŽ•ā¯ āŽ†āŽšāŽŋāŽ°āŽŋāŽ¯āŽ°ā¯", + "json_error": "āŽšāŽžāŽ¤ā¯ŠāŽĒā¯ŠāŽ•ā¯ āŽĒāŽŋāŽ´ā¯ˆ", "keep": "āŽĩā¯ˆāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ™ā¯āŽ•āŽŗā¯", + "keep_albums": "āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ˆ āŽĩā¯ˆāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ™ā¯āŽ•āŽŗā¯", + "keep_albums_count": "{count} {count, plural, one {āŽ¤ā¯ŠāŽ•ā¯āŽĒā¯āŽĒ❁} other {āŽ¤ā¯ŠāŽ•ā¯āŽĒā¯āŽĒā¯āŽ•āŽŗā¯}} āŽĩā¯ˆāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯", "keep_all": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ˆāŽ¯ā¯āŽŽā¯ āŽĩā¯ˆāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ™ā¯āŽ•āŽŗā¯", + "keep_description": "āŽ‡āŽŸāŽ¤ā¯āŽ¤ā¯ˆāŽ•ā¯ āŽ•āŽžāŽ˛āŽŋāŽ¯āŽžāŽ•ā¯āŽ•ā¯āŽŽā¯ āŽĒā¯‹āŽ¤ā¯ āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽšāŽžāŽ¤āŽŠāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽŽāŽŠā¯āŽŠ āŽ‡āŽ°ā¯āŽ•ā¯āŽ• āŽĩā¯‡āŽŖā¯āŽŸā¯āŽŽā¯ āŽŽāŽŠā¯āŽĒāŽ¤ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽĩā¯āŽšā¯†āŽ¯ā¯āŽ¯āŽĩā¯āŽŽā¯.", + "keep_favorites": "āŽĒāŽŋāŽŸāŽŋāŽ¤ā¯āŽ¤āŽĩā¯ˆāŽ•āŽŗā¯ˆ āŽĩā¯ˆāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ™ā¯āŽ•āŽŗā¯", + "keep_on_device": "āŽšāŽžāŽ¤āŽŠāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽĩā¯ˆāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ™ā¯āŽ•āŽŗā¯", + "keep_on_device_hint": "āŽ‡āŽ¨ā¯āŽ¤āŽšā¯ āŽšāŽžāŽ¤āŽŠāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽĩā¯ˆāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ•ā¯āŽ• āŽĩā¯‡āŽŖā¯āŽŸāŽŋāŽ¯ āŽĒā¯ŠāŽ°ā¯āŽŸā¯āŽ•āŽŗā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "keep_this_delete_others": "āŽ‡āŽ¤ā¯ˆ āŽĩā¯ˆāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ™ā¯āŽ•āŽŗā¯, āŽŽāŽąā¯āŽąāŽĩāŽ°ā¯āŽ•āŽŗā¯ˆ āŽ¨ā¯€āŽ•ā¯āŽ•ā¯", + "keeping": "āŽĩā¯ˆāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤āŽ˛ā¯: {items}", "kept_this_deleted_others": "āŽ‡āŽ¨ā¯āŽ¤āŽšā¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ˆ āŽĩā¯ˆāŽ¤ā¯āŽ¤ā¯, {count, plural, one {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯} other {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•āŽŗā¯}} āŽ¨ā¯€āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "keyboard_shortcuts": "āŽĩāŽŋāŽšā¯ˆāŽĒā¯āŽĒāŽ˛āŽ•ā¯ˆ āŽ•ā¯āŽąā¯āŽ•ā¯āŽ•ā¯āŽĩāŽ´āŽŋāŽ•āŽŗā¯", "language": "āŽŽā¯ŠāŽ´āŽŋ", @@ -1287,6 +1394,7 @@ "local": "āŽ‰āŽŗā¯āŽŗāŽ•", "local_asset_cast_failed": "āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ•āŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽĒā¯āŽĒāŽŸāŽžāŽ¤ āŽ’āŽ°ā¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ˆ āŽ…āŽŠā¯āŽĒā¯āŽĒ āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "local_assets": "āŽ‰āŽŗā¯āŽŗāŽ• āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯", + "local_id": "āŽ‰āŽŗā¯āŽŗāŽ• āŽ…āŽŸā¯ˆāŽ¯āŽžāŽŗāŽŽā¯", "local_media_summary": "āŽ‰āŽŗā¯āŽŗāŽ• āŽŠāŽŸāŽ• āŽšā¯āŽ°ā¯āŽ•ā¯āŽ•āŽŽā¯", "local_network": "āŽ‰āŽŗā¯āŽŗāŽ• āŽĒāŽŋāŽŖā¯ˆāŽ¯āŽŽā¯", "local_network_sheet_info": "āŽ•ā¯āŽąāŽŋāŽĒā¯āŽĒāŽŋāŽŸā¯āŽŸ āŽĩā¯ˆāŽƒāŽĒ❈ āŽ¨ā¯†āŽŸā¯āŽĩā¯ŠāŽ°ā¯āŽ•ā¯āŽ•ā¯ˆāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤ā¯āŽŽā¯ āŽĒā¯‹āŽ¤ā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯ āŽ‡āŽ¨ā¯āŽ¤ āŽŽā¯āŽ•āŽĩāŽ°āŽŋ āŽŽā¯‚āŽ˛āŽŽā¯ āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ•āŽ¤ā¯āŽ¤ā¯āŽŸāŽŠā¯ āŽ‡āŽŖā¯ˆāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯", @@ -1338,10 +1446,28 @@ "loop_videos_description": "āŽĩāŽŋāŽ°āŽŋāŽĩāŽžāŽŠ āŽĒāŽžāŽ°ā¯āŽĩā¯ˆāŽ¯āŽžāŽŗāŽ°āŽŋāŽ˛ā¯ āŽ’āŽ°ā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽĩ❈ āŽ¤āŽžāŽŠāŽžāŽ• āŽĩāŽŗā¯ˆāŽ¯āŽĒā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯.", "main_branch_warning": "āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽ’āŽ°ā¯ āŽŽā¯‡āŽŽā¯āŽĒāŽžāŽŸā¯āŽŸā¯ āŽĒāŽ¤āŽŋāŽĒā¯āŽĒ❈āŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤ā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗā¯; āŽĩā¯†āŽŗāŽŋāŽ¯ā¯€āŽŸā¯āŽŸā¯ āŽĒāŽ¤āŽŋāŽĒā¯āŽĒ❈āŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤ āŽ¨āŽžāŽ™ā¯āŽ•āŽŗā¯ āŽ•āŽŸā¯āŽŽā¯ˆāŽ¯āŽžāŽ• āŽĒāŽ°āŽŋāŽ¨ā¯āŽ¤ā¯āŽ°ā¯ˆāŽ•ā¯āŽ•āŽŋāŽąā¯‹āŽŽā¯!", "main_menu": "āŽĒāŽŸā¯āŽŸāŽŋāŽ¯āŽ˛ā¯ āŽĩāŽŋāŽŗā¯ˆāŽ¯āŽžāŽŸā¯āŽ™ā¯āŽ•āŽŗā¯", + "maintenance_action_restore": "āŽ¤āŽ°āŽĩā¯āŽ¤ā¯āŽ¤āŽŗāŽ¤ā¯āŽ¤ā¯ˆ āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆāŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯", "maintenance_description": "āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯ āŽĒāŽ°āŽžāŽŽāŽ°āŽŋāŽĒā¯āŽĒ❁ āŽĒāŽ¯āŽŠā¯āŽŽā¯āŽąā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽĩā¯ˆāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸā¯āŽŗā¯āŽŗāŽ¤ā¯.", "maintenance_end": "āŽĒāŽ°āŽžāŽŽāŽ°āŽŋāŽĒā¯āŽĒ❁ āŽĒāŽ¯āŽŠā¯āŽŽā¯āŽąā¯ˆāŽ¯ā¯ˆ āŽŽā¯āŽŸāŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "maintenance_end_error": "āŽĒāŽ°āŽžāŽŽāŽ°āŽŋāŽĒā¯āŽĒ❁ āŽŽā¯āŽąā¯ˆāŽ¯ā¯ˆ āŽŽā¯āŽŸāŽŋāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ.", "maintenance_logged_in_as": "āŽ¤āŽąā¯āŽĒā¯‹āŽ¤ā¯ {user} āŽ†āŽ• āŽ‰āŽŗā¯āŽ¨ā¯āŽ´ā¯ˆāŽ¨ā¯āŽ¤ā¯āŽŗā¯āŽŗā¯€āŽ°ā¯āŽ•āŽŗā¯", + "maintenance_restore_from_backup": "āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋāŽ¯āŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆ", + "maintenance_restore_library": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ¨ā¯‚āŽ˛āŽ•āŽ¤ā¯āŽ¤ā¯ˆ āŽŽā¯€āŽŸā¯āŽŸā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "maintenance_restore_library_confirm": "āŽ‡āŽ¤ā¯ āŽšāŽ°āŽŋāŽ¯āŽžāŽ•āŽ¤ā¯ āŽ¤ā¯‹āŽŠā¯āŽąāŽŋāŽŠāŽžāŽ˛ā¯, āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋāŽ¯ā¯ˆ āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆāŽĒā¯āŽĒāŽ¤ā¯ˆāŽ¤ā¯ āŽ¤ā¯ŠāŽŸāŽ°āŽĩā¯āŽŽā¯!", + "maintenance_restore_library_description": "āŽ¤āŽ°āŽĩā¯āŽ¤ā¯āŽ¤āŽŗāŽ¤ā¯āŽ¤ā¯ˆ āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆāŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯", + "maintenance_restore_library_folder_has_files": "{folder}āŽ˛ā¯ {count} āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽąā¯ˆ(āŽ•āŽŗā¯) āŽ‰āŽŗā¯āŽŗāŽ¤ā¯", + "maintenance_restore_library_folder_no_files": "{folder} āŽ‡āŽ˛ā¯ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽ•āŽŗā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ!", + "maintenance_restore_library_folder_pass": "āŽĒāŽŸāŽŋāŽ•ā¯āŽ•āŽ•ā¯āŽ•ā¯‚āŽŸāŽŋāŽ¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽŽāŽ´ā¯āŽ¤āŽ•ā¯āŽ•ā¯‚āŽŸāŽŋāŽ¯", + "maintenance_restore_library_folder_read_fail": "āŽĒāŽŸāŽŋāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽ¤ā¯", + "maintenance_restore_library_folder_write_fail": "āŽŽāŽ´ā¯āŽ¤ āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽ¤ā¯", + "maintenance_restore_library_hint_missing_files": "āŽŽā¯āŽ•ā¯āŽ•āŽŋāŽ¯āŽŽāŽžāŽŠ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽ•āŽžāŽŖāŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", + "maintenance_restore_library_hint_regenerate_later": "āŽ‡āŽĩāŽąā¯āŽąā¯ˆ āŽĒāŽŋāŽŠā¯āŽŠāŽ°ā¯ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗāŽŋāŽ˛ā¯ āŽŽā¯€āŽŖā¯āŽŸā¯āŽŽā¯ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽ˛āŽžāŽŽā¯", + "maintenance_restore_library_hint_storage_template_missing_files": "āŽšā¯‡āŽŽāŽŋāŽĒā¯āŽĒāŽ• āŽŸā¯†āŽŽā¯āŽĒā¯āŽŗā¯‡āŽŸā¯āŽŸā¯ˆāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤ā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗāŽž? āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽ•āŽžāŽŖāŽžāŽŽāŽ˛ā¯ āŽ‡āŽ°ā¯āŽ•ā¯āŽ•āŽ˛āŽžāŽŽā¯", + "maintenance_restore_library_loading": "āŽ’āŽ°ā¯āŽŽā¯ˆāŽĒā¯āŽĒāŽžāŽŸā¯ āŽ•āŽžāŽšā¯‹āŽ˛ā¯ˆāŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽŠāŽ°āŽŋāŽšā¯āŽŸāŽŋāŽ•ā¯āŽšā¯ āŽāŽąā¯āŽąā¯āŽ•āŽŋāŽąāŽ¤ā¯â€Ļ", + "maintenance_task_backup": "āŽāŽąā¯āŽ•āŽŠāŽĩ❇ āŽ‰āŽŗā¯āŽŗ āŽ¤āŽ°āŽĩā¯āŽ¤ā¯āŽ¤āŽŗāŽ¤ā¯āŽ¤āŽŋāŽŠā¯ āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋāŽ¯ā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯â€Ļ", + "maintenance_task_migrations": "āŽ¤āŽ°āŽĩā¯āŽ¤ā¯āŽ¤āŽŗ āŽ¨āŽ•āŽ°ā¯āŽĩā¯āŽ•āŽŗā¯ˆ āŽ‡āŽ¯āŽ•ā¯āŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯â€Ļ", + "maintenance_task_restore": "āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋāŽ¯ā¯ˆ āŽŽā¯€āŽŸā¯āŽŸā¯†āŽŸā¯āŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯â€Ļ", + "maintenance_task_rollback": "āŽŽā¯€āŽŸā¯āŽŸā¯†āŽŸā¯āŽĒā¯āŽĒ❁ āŽ¤ā¯‹āŽ˛ā¯āŽĩāŽŋāŽ¯āŽŸā¯ˆāŽ¨ā¯āŽ¤āŽ¤ā¯, āŽĒā¯āŽŗā¯āŽŗāŽŋāŽ¯ā¯ˆ āŽŽā¯€āŽŸā¯āŽŸā¯†āŽŸā¯āŽ•ā¯āŽ• āŽŽā¯€āŽŖā¯āŽŸā¯āŽŽā¯ āŽ‰āŽ°ā¯āŽŸā¯āŽŸā¯āŽ•āŽŋāŽąāŽ¤ā¯â€Ļ", "maintenance_title": "āŽ¤āŽąā¯āŽ•āŽžāŽ˛āŽŋāŽ•āŽŽāŽžāŽ• āŽ•āŽŋāŽŸā¯ˆāŽ•ā¯āŽ•āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "make": "āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•ā¯", "manage_geolocation": "āŽ‡āŽ°ā¯āŽĒā¯āŽĒāŽŋāŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -1403,6 +1529,8 @@ "minimize": "āŽ•ā¯āŽąā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "minute": "āŽ¨āŽŋāŽŽāŽŋāŽŸāŽ™ā¯āŽ•āŽŗā¯", "minutes": "āŽ¨āŽŋāŽŽāŽŋāŽŸāŽ™ā¯āŽ•āŽŗā¯", + "mirror_horizontal": "āŽ•āŽŋāŽŸā¯ˆāŽŽāŽŸā¯āŽŸ", + "mirror_vertical": "āŽšā¯†āŽ™ā¯āŽ•ā¯āŽ¤ā¯āŽ¤ā¯", "missing": "āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ", "mobile_app": "āŽŽā¯ŠāŽĒā¯ˆāŽ˛ā¯ āŽ†āŽĒā¯", "mobile_app_download_onboarding_note": "āŽĒāŽŋāŽŠā¯āŽĩāŽ°ā¯āŽŽā¯ āŽĩāŽŋāŽ°ā¯āŽĒā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ˆāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽŋ āŽ¤ā¯āŽŖā¯ˆ āŽŽā¯ŠāŽĒā¯ˆāŽ˛ā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯āŽŸā¯ˆāŽĒā¯ āŽĒāŽ¤āŽŋāŽĩāŽŋāŽąāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -1411,11 +1539,14 @@ "monthly_title_text_date_format": "Mmmm āŽ’āŽ¯ā¯", "more": "āŽŽā¯‡āŽ˛ā¯āŽŽā¯", "move": "āŽ¨āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", + "move_down": "āŽ•ā¯€āŽ´ā¯‡ āŽ¨āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", "move_off_locked_folder": "āŽĒā¯‚āŽŸā¯āŽŸāŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽąā¯ˆāŽ¯āŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽĩā¯†āŽŗāŽŋāŽ¯ā¯‡āŽąāŽĩā¯āŽŽā¯", "move_to": "āŽ¨āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤ā¯", + "move_to_device_trash": "āŽšāŽžāŽ¤āŽŠāŽ¤ā¯āŽ¤āŽŋāŽŠā¯ āŽ•ā¯āŽĒā¯āŽĒā¯ˆāŽ•ā¯āŽ•ā¯ āŽ¨āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", "move_to_lock_folder_action_prompt": "āŽĒā¯‚āŽŸā¯āŽŸāŽŋāŽ¯ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽąā¯ˆāŽ¯āŽŋāŽ˛ā¯ {count} āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "move_to_locked_folder": "āŽĒā¯‚āŽŸā¯āŽŸāŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽąā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽšā¯†āŽ˛ā¯āŽ˛ā¯āŽ™ā¯āŽ•āŽŗā¯", "move_to_locked_folder_confirmation": "āŽ‡āŽ¨ā¯āŽ¤ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹ āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗāŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯āŽŽā¯ āŽ…āŽ•āŽąā¯āŽąāŽĒā¯āŽĒāŽŸā¯āŽŽā¯, āŽŽā¯‡āŽ˛ā¯āŽŽā¯ āŽĒā¯‚āŽŸā¯āŽŸāŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽąā¯ˆāŽ¯āŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽŽāŽŸā¯āŽŸā¯āŽŽā¯‡ āŽĒāŽžāŽ°ā¯āŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯ā¯āŽŽā¯", + "move_up": "āŽŽā¯‡āŽ˛ā¯‡ āŽšā¯†āŽ˛ā¯āŽ˛āŽĩā¯āŽŽā¯", "moved_to_archive": "{count, plural, one {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯} other {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯}} āŽ•āŽžāŽĒā¯āŽĒāŽ•āŽ¤ā¯āŽ¤āŽŋāŽąā¯āŽ•ā¯ āŽ¨āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "moved_to_library": "{count, plural, one {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯} other {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯}} āŽ¨ā¯‚āŽ˛āŽ•āŽ¤ā¯āŽ¤āŽŋāŽąā¯āŽ•ā¯ āŽ¨āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "moved_to_trash": "āŽ•ā¯āŽĒā¯āŽĒā¯ˆāŽ•ā¯āŽ•ā¯ āŽ¨āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", @@ -1425,6 +1556,7 @@ "my_albums": "āŽŽāŽŠāŽ¤ā¯ āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯", "name": "āŽĒā¯†āŽ¯āŽ°ā¯", "name_or_nickname": "āŽĒā¯†āŽ¯āŽ°ā¯ āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽĒā¯āŽŠā¯ˆāŽĒā¯āŽĒā¯†āŽ¯āŽ°ā¯", + "name_required": "āŽĒā¯†āŽ¯āŽ°ā¯ āŽ¤ā¯‡āŽĩ❈", "navigate": "āŽĩāŽ´āŽŋāŽšā¯†āŽ˛ā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", "navigate_to_time": "āŽ¨ā¯‡āŽ°āŽ¤ā¯āŽ¤āŽŋāŽąā¯āŽ•ā¯ āŽšā¯†āŽ˛ā¯āŽ˛āŽĩā¯āŽŽā¯", "network_requirement_photos_upload": "āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯ āŽšā¯†āŽ˛ā¯āŽ˛ā¯āŽ˛āŽžāŽ°ā¯ āŽ¤āŽ°āŽĩ❈āŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", @@ -1449,20 +1581,24 @@ "next": "āŽ…āŽŸā¯āŽ¤ā¯āŽ¤āŽ¤ā¯", "next_memory": "āŽ…āŽŸā¯āŽ¤ā¯āŽ¤ āŽ¨āŽŋāŽŠā¯ˆāŽĩāŽ•āŽŽā¯", "no": "āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ", + "no_actions_added": "āŽ‡āŽŠā¯āŽŠā¯āŽŽā¯ āŽšā¯†āŽ¯āŽ˛ā¯āŽ•āŽŗā¯ āŽŽāŽ¤ā¯āŽĩā¯āŽŽā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", + "no_albums_found": "āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽ¤ā¯āŽĩā¯āŽŽā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ", "no_albums_message": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ˆ āŽ’āŽ´ā¯āŽ™ā¯āŽ•āŽŽā¯ˆāŽ•ā¯āŽ• āŽ’āŽ°ā¯ āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤ā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "no_albums_with_name_yet": "āŽ‡āŽ¨ā¯āŽ¤ āŽĒā¯†āŽ¯āŽ°ā¯āŽŸāŽŠā¯ āŽ‡āŽŠā¯āŽŠā¯āŽŽā¯ āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽ¤ā¯āŽĩā¯āŽŽā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ āŽŽāŽŠā¯āŽąā¯ āŽ¤ā¯†āŽ°āŽŋāŽ•āŽŋāŽąāŽ¤ā¯.", "no_albums_yet": "āŽ‰āŽ™ā¯āŽ•āŽŗāŽŋāŽŸāŽŽā¯ āŽ‡āŽ¤ā¯āŽĩāŽ°ā¯ˆ āŽŽāŽ¨ā¯āŽ¤ āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯āŽŽā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ āŽŽāŽŠā¯āŽąā¯ āŽ¤ā¯†āŽ°āŽŋāŽ•āŽŋāŽąāŽ¤ā¯.", "no_archived_assets_message": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ•ā¯ āŽ•āŽžāŽŸā¯āŽšāŽŋāŽ¯āŽŋāŽ˛ā¯ āŽ‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽ…āŽĩāŽąā¯āŽąā¯ˆ āŽŽāŽąā¯ˆāŽ•ā¯āŽ• āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ˆ āŽ•āŽžāŽĒā¯āŽĒāŽ•āŽĒā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", - "no_assets_message": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽŽā¯āŽ¤āŽ˛ā¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽą āŽšā¯ŠāŽŸā¯āŽ•ā¯āŽ•ā¯ āŽšā¯†āŽ¯ā¯āŽ•", + "no_assets_message": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽŽā¯āŽ¤āŽ˛ā¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽą āŽšā¯ŠāŽŸā¯āŽ•ā¯āŽ•ā¯ āŽšā¯†āŽ¯ā¯āŽ¯āŽĩā¯āŽŽā¯", "no_assets_to_show": "āŽ•āŽžāŽŸā¯āŽŸ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ", "no_cast_devices_found": "āŽ¨āŽŸāŽŋāŽ•āŽ°ā¯āŽ•āŽŗā¯ āŽšāŽžāŽ¤āŽŠāŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽ¤ā¯āŽĩā¯āŽŽā¯ āŽ•āŽŋāŽŸā¯ˆāŽ•ā¯āŽ•āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "no_checksum_local": "āŽšā¯†āŽ•ā¯āŽšāŽŽā¯ āŽŽāŽ¤ā¯āŽĩā¯āŽŽā¯ āŽ•āŽŋāŽŸā¯ˆāŽ•ā¯āŽ•āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ - āŽ‰āŽŗā¯āŽŗāŽ• āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ˆāŽĒā¯ āŽĒā¯†āŽą āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽ¤ā¯", "no_checksum_remote": "āŽšā¯†āŽ•ā¯āŽšāŽŽā¯ āŽŽāŽ¤ā¯āŽĩā¯āŽŽā¯ āŽ•āŽŋāŽŸā¯ˆāŽ•ā¯āŽ•āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ - āŽ¤ā¯ŠāŽ˛ā¯ˆ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽĒā¯†āŽą āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽ¤ā¯", + "no_configuration_needed": "āŽ•āŽŸā¯āŽŸāŽŽā¯ˆāŽĒā¯āŽĒ❁ āŽ¤ā¯‡āŽĩā¯ˆāŽ¯āŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "no_devices": "āŽ…āŽ™ā¯āŽ•ā¯€āŽ•āŽ°āŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽšāŽžāŽ¤āŽŠāŽ™ā¯āŽ•āŽŗā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ", "no_duplicates_found": "āŽ¨āŽ•āŽ˛ā¯āŽ•āŽŗā¯ āŽŽāŽ¤ā¯āŽĩā¯āŽŽā¯ āŽ•āŽžāŽŖāŽĒā¯āŽĒāŽŸāŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ.", - "no_exif_info_available": "EXIF āŽšā¯†āŽ¯ā¯āŽ¤āŽŋ āŽŽāŽ¤ā¯āŽĩā¯āŽŽā¯ āŽ•āŽŋāŽŸā¯ˆāŽ•ā¯āŽ•āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", + "no_exif_info_available": "exif āŽšā¯†āŽ¯ā¯āŽ¤āŽŋ āŽŽāŽ¤ā¯āŽĩā¯āŽŽā¯ āŽ•āŽŋāŽŸā¯ˆāŽ•ā¯āŽ•āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "no_explore_results_message": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ¤ā¯ŠāŽ•ā¯āŽĒā¯āŽĒ❈ āŽ†āŽ°āŽžāŽ¯ āŽ•ā¯‚āŽŸā¯āŽ¤āŽ˛ā¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ˆ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽĩā¯āŽŽā¯.", "no_favorites_message": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽšāŽŋāŽąāŽ¨ā¯āŽ¤ āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ˆ āŽĩāŽŋāŽ°ā¯ˆāŽĩāŽžāŽ•āŽ•ā¯ āŽ•āŽŖā¯āŽŸā¯āŽĒāŽŋāŽŸāŽŋāŽ•ā¯āŽ• āŽĒāŽŋāŽŸāŽŋāŽ¤ā¯āŽ¤āŽĩā¯ˆāŽ•āŽŗā¯ˆāŽšā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "no_filters_added": "āŽ‡āŽ¤ā¯āŽĩāŽ°ā¯ˆ āŽĩāŽŸāŽŋāŽĒā¯āŽĒāŽžāŽŠā¯āŽ•āŽŗā¯ āŽŽāŽ¤ā¯āŽĩā¯āŽŽā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "no_libraries_message": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ˆāŽ•ā¯ āŽ•āŽžāŽŖ āŽĩā¯†āŽŗāŽŋāŽĒā¯āŽĒā¯āŽą āŽ¨ā¯‚āŽ˛āŽ•āŽ¤ā¯āŽ¤ā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "no_local_assets_found": "āŽ‡āŽ¨ā¯āŽ¤ āŽšā¯†āŽ•ā¯āŽšāŽŽā¯ āŽŽā¯‚āŽ˛āŽŽā¯ āŽ‰āŽŗā¯āŽŗāŽ• āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ āŽŽāŽ¤ā¯āŽĩā¯āŽŽā¯ āŽ•āŽžāŽŖāŽĒā¯āŽĒāŽŸāŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "no_location_set": "āŽ‡āŽŸāŽŽā¯ āŽ…āŽŽā¯ˆāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", @@ -1476,6 +1612,7 @@ "no_results_description": "āŽ’āŽ°ā¯ āŽ’āŽ¤ā¯āŽ¤ āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽĒā¯ŠāŽ¤ā¯āŽĩāŽžāŽŠ āŽŽā¯āŽ•ā¯āŽ•āŽŋāŽ¯ āŽšā¯ŠāŽ˛ā¯āŽ˛ā¯ˆ āŽŽā¯āŽ¯āŽąā¯āŽšāŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "no_shared_albums_message": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ¨ā¯†āŽŸā¯āŽĩā¯ŠāŽ°ā¯āŽ•ā¯āŽ•āŽŋāŽ˛ā¯ āŽ‰āŽŗā¯āŽŗāŽĩāŽ°ā¯āŽ•āŽŗā¯āŽŸāŽŠā¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽĒāŽ•āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽ•ā¯ŠāŽŗā¯āŽŗ āŽ’āŽ°ā¯ āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤ā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "no_uploads_in_progress": "āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽ™ā¯āŽ•āŽŗā¯ āŽŽā¯āŽŠā¯āŽŠā¯‡āŽąā¯āŽąāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ", + "none": "āŽŽāŽ¤ā¯āŽĩā¯āŽŽāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "not_allowed": "āŽ…āŽŠā¯āŽŽāŽ¤āŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "not_available": "āŽ‡āŽ¤āŽąā¯āŽ•āŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "not_in_any_album": "āŽŽāŽ¨ā¯āŽ¤ āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯āŽŽā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ", @@ -1509,6 +1646,8 @@ "online": "āŽ†āŽŠā¯āŽ˛ā¯ˆāŽŠāŽŋāŽ˛ā¯", "only_favorites": "āŽĒāŽŋāŽŸāŽŋāŽ¤ā¯āŽ¤āŽĩ❈ āŽŽāŽŸā¯āŽŸā¯āŽŽā¯‡", "open": "āŽ¤āŽŋāŽą", + "open_calendar": "āŽ•āŽžāŽ˛ā¯†āŽŖā¯āŽŸāŽ°ā¯ˆāŽ¤ā¯ āŽ¤āŽŋāŽąāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "open_in_browser": "āŽ‰āŽ˛āŽžāŽĩāŽŋāŽ¯āŽŋāŽ˛ā¯ āŽ¤āŽŋāŽą", "open_in_map_view": "āŽĩāŽ°ā¯ˆāŽĒāŽŸāŽ•ā¯ āŽ•āŽžāŽŸā¯āŽšāŽŋāŽ¯āŽŋāŽ˛ā¯ āŽ¤āŽŋāŽąāŽ¨ā¯āŽ¤āŽŋāŽ°ā¯āŽ•ā¯āŽ•ā¯āŽŽā¯", "open_in_openstreetmap": "OpenStreetMap āŽ‡āŽ˛ā¯ āŽ¤āŽŋāŽąāŽ¨ā¯āŽ¤āŽŋāŽ°ā¯āŽ•ā¯āŽ•ā¯āŽŽā¯", "open_the_search_filters": "āŽ¤ā¯‡āŽŸāŽ˛ā¯ āŽĩāŽŸāŽŋāŽĒā¯āŽĒāŽžāŽŠā¯āŽ•āŽŗā¯ˆāŽ¤ā¯ āŽ¤āŽŋāŽąāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -1557,6 +1696,7 @@ "people": "āŽŽāŽ•ā¯āŽ•āŽŗā¯", "people_edits_count": "{count, plural, one {# āŽ¨āŽĒāŽ°ā¯} other {# āŽĒā¯‡āŽ°ā¯}} āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "people_feature_description": "āŽŽāŽ•ā¯āŽ•āŽŗā¯ āŽ¤ā¯ŠāŽ•ā¯āŽ¤ā¯āŽ¤ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ˆ āŽ‰āŽ˛āŽžāŽĩā¯āŽ¤āŽ˛ā¯", + "people_selected": "{count, plural, one {# āŽ¨āŽĒāŽ°ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽžāŽ°ā¯} other {# āŽĒā¯‡āŽ°ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŠāŽ°ā¯}}", "people_sidebar_description": "āŽĒāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŋāŽ¯āŽŋāŽ˛ā¯ āŽ‰āŽŗā¯āŽŗāŽĩāŽ°ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯ āŽ’āŽ°ā¯ āŽ‡āŽŖā¯ˆāŽĒā¯āŽĒā¯ˆāŽ•ā¯ āŽ•āŽžāŽŖā¯āŽĒāŽŋ", "permanent_deletion_warning": "āŽ¨āŽŋāŽ°āŽ¨ā¯āŽ¤āŽ° āŽ¨ā¯€āŽ•ā¯āŽ•ā¯āŽ¤āŽ˛ā¯ āŽŽāŽšā¯āŽšāŽ°āŽŋāŽ•ā¯āŽ•ā¯ˆ", "permanent_deletion_warning_setting_description": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ˆ āŽ¨āŽŋāŽ°āŽ¨ā¯āŽ¤āŽ°āŽŽāŽžāŽ• āŽ¨ā¯€āŽ•ā¯āŽ•ā¯āŽŽā¯āŽĒā¯‹āŽ¤ā¯ āŽ’āŽ°ā¯ āŽŽāŽšā¯āŽšāŽ°āŽŋāŽ•ā¯āŽ•ā¯ˆāŽ¯ā¯ˆāŽ•ā¯ āŽ•āŽžāŽŸā¯āŽŸā¯āŽ™ā¯āŽ•āŽŗā¯", @@ -1581,11 +1721,14 @@ "person_age_years": "{years, plural, other {# āŽ†āŽŖā¯āŽŸā¯āŽ•āŽŗā¯}} āŽĒāŽ´ā¯ˆāŽ¯āŽ¤ā¯", "person_birthdate": "{date} āŽ‡āŽ˛ā¯ āŽĒāŽŋāŽąāŽ¨ā¯āŽ¤āŽžāŽ°ā¯", "person_hidden": "{name}{hidden, select, true { (āŽŽāŽąā¯ˆāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ)} other {}}", + "person_recognized": "āŽ…āŽŸā¯ˆāŽ¯āŽžāŽŗāŽŽā¯ āŽ•āŽžāŽŖāŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ¨āŽĒāŽ°ā¯", + "person_selected": "āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ¨āŽĒāŽ°ā¯", "photo_shared_all_users": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ˆ āŽŽāŽ˛ā¯āŽ˛āŽž āŽĒāŽ¯āŽŠāŽ°ā¯āŽ•āŽŗā¯āŽŸāŽŠā¯āŽŽā¯ āŽĒāŽ•āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽ•ā¯ŠāŽŖā¯āŽŸāŽ¤āŽžāŽ•āŽ¤ā¯ āŽ¤ā¯†āŽ°āŽŋāŽ•āŽŋāŽąāŽ¤ā¯ āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽĒāŽ•āŽŋāŽ°ā¯āŽĩāŽ¤āŽąā¯āŽ•ā¯ āŽ‰āŽ™ā¯āŽ•āŽŗāŽŋāŽŸāŽŽā¯ āŽŽāŽ¨ā¯āŽ¤ āŽĒāŽ¯āŽŠāŽ°ā¯āŽŽā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ.", "photos": "āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯", "photos_and_videos": "āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ & āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯", "photos_count": "{count, plural, one {{count, number} āŽĒāŽŸāŽŽā¯} other {{count, number} āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯}}", "photos_from_previous_years": "āŽŽā¯āŽ¨ā¯āŽ¤ā¯ˆāŽ¯ āŽ†āŽŖā¯āŽŸā¯āŽ•āŽŗāŽŋāŽŠā¯ āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯", + "photos_only": "āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽŸā¯āŽŸā¯āŽŽā¯‡", "pick_a_location": "āŽ’āŽ°ā¯ āŽ‡āŽŸāŽ¤ā¯āŽ¤ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ™ā¯āŽ•āŽŗā¯", "pick_custom_range": "āŽ¤āŽŠāŽŋāŽĒā¯āŽĒāŽ¯āŽŠā¯ āŽĩāŽ°āŽŽā¯āŽĒ❁", "pick_date_range": "āŽ¤ā¯‡āŽ¤āŽŋ āŽĩāŽ°āŽŽā¯āŽĒā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -1661,9 +1804,10 @@ "purchase_settings_server_activated": "āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ• āŽ¤āŽ¯āŽžāŽ°āŽŋāŽĒā¯āŽĒ❁ āŽĩāŽŋāŽšā¯ˆ āŽ¨āŽŋāŽ°ā¯āŽĩāŽžāŽ•āŽŋāŽ¯āŽžāŽ˛ā¯ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽ•āŽŋāŽąāŽ¤ā¯", "query_asset_id": "āŽĩāŽŋāŽŠāŽĩāŽ˛ā¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽ…āŽŸā¯ˆāŽ¯āŽžāŽŗāŽŽā¯", "queue_status": "āŽĩāŽ°āŽŋāŽšā¯ˆ {count}/{total}", + "rate_asset": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽŽāŽ¤āŽŋāŽĒā¯āŽĒ❁", "rating": "āŽ¨āŽŸā¯āŽšāŽ¤ā¯āŽ¤āŽŋāŽ° āŽŽāŽ¤āŽŋāŽĒā¯āŽĒā¯€āŽŸā¯", "rating_clear": "āŽ¤ā¯†āŽŗāŽŋāŽĩāŽžāŽŠ āŽŽāŽ¤āŽŋāŽĒā¯āŽĒā¯€āŽŸā¯", - "rating_count": "{count, plural, one {# āŽĩāŽŋāŽŖā¯āŽŽā¯€āŽŠā¯} other {# āŽĩāŽŋāŽŖā¯āŽŽā¯€āŽŠā¯āŽ•āŽŗā¯}}", + "rating_count": "{count, plural, =0 {āŽŽāŽ¤āŽŋāŽĒāŽŋāŽŸāŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ} one {# āŽĩāŽŋāŽŖā¯āŽŽā¯€āŽŠā¯} other {# āŽĩāŽŋāŽŖā¯āŽŽā¯€āŽŠā¯āŽ•āŽŗā¯}}", "rating_description": "āŽšā¯†āŽ¯ā¯āŽ¤āŽŋ āŽ•ā¯āŽ´ā¯āŽĩāŽŋāŽ˛ā¯ EXIF āŽŽāŽ¤āŽŋāŽĒā¯āŽĒā¯€āŽŸā¯āŽŸā¯ˆāŽ•ā¯ āŽ•āŽžāŽŖā¯āŽĒāŽŋ", "reaction_options": "āŽŽāŽ¤āŽŋāŽ°ā¯āŽĩāŽŋāŽŠā¯ˆ āŽĩāŽŋāŽ°ā¯āŽĒā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯", "read_changelog": "āŽšā¯‡āŽžā¯āŽšā¯āŽ˛āŽžāŽ•ā¯ āŽĒāŽŸāŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -1736,7 +1880,10 @@ "reset_pin_code_success": "āŽŽā¯āŽŗā¯ āŽ•ā¯āŽąāŽŋāŽ¯ā¯€āŽŸā¯āŽŸā¯ˆ āŽĩā¯†āŽąā¯āŽąāŽŋāŽ•āŽ°āŽŽāŽžāŽ• āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "reset_pin_code_with_password": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ•āŽŸāŽĩā¯āŽšā¯āŽšā¯ŠāŽ˛ā¯ āŽŽā¯‚āŽ˛āŽŽā¯ āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽŽā¯āŽŗā¯ āŽ•ā¯āŽąāŽŋāŽ¯ā¯€āŽŸā¯āŽŸā¯ˆ āŽŽāŽĒā¯āŽĒā¯‹āŽ¤ā¯āŽŽā¯ āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆāŽ•ā¯āŽ•āŽ˛āŽžāŽŽā¯", "reset_sqlite": "SQLite āŽ¤āŽ°āŽĩā¯āŽ¤ā¯āŽ¤āŽŗāŽ¤ā¯āŽ¤ā¯ˆ āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", - "reset_sqlite_confirmation": "SQLITE āŽ¤āŽ°āŽĩā¯āŽ¤ā¯āŽ¤āŽŗāŽ¤ā¯āŽ¤ā¯ˆ āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆāŽ•ā¯āŽ• āŽĩāŽŋāŽ°ā¯āŽŽā¯āŽĒā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗāŽž? āŽ¤āŽ°āŽĩ❈ āŽŽā¯€āŽŖā¯āŽŸā¯āŽŽā¯ āŽ’āŽ¤ā¯āŽ¤āŽŋāŽšā¯ˆāŽ•ā¯āŽ• āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽĩā¯†āŽŗāŽŋāŽ¯ā¯‡āŽąāŽŋ āŽŽā¯€āŽŖā¯āŽŸā¯āŽŽā¯ āŽ‰āŽŗā¯āŽ¨ā¯āŽ´ā¯ˆāŽ¯ āŽĩā¯‡āŽŖā¯āŽŸā¯āŽŽā¯", + "reset_sqlite_clear_app_data": "āŽ¤āŽ°āŽĩ❈ āŽ…āŽ´āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "reset_sqlite_confirmation": "āŽ†āŽĒā¯āŽšā¯ āŽ¤āŽ°āŽĩ❈ āŽ¨āŽŋāŽšā¯āŽšāŽ¯āŽŽāŽžāŽ• āŽ…āŽ´āŽŋāŽ•ā¯āŽ• āŽĩāŽŋāŽ°ā¯āŽŽā¯āŽĒā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗāŽž? āŽ‡āŽ¤ā¯ āŽŽāŽ˛ā¯āŽ˛āŽž āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽ¨ā¯€āŽ•ā¯āŽ•āŽŋāŽĩāŽŋāŽŸā¯āŽŸā¯ āŽ‰āŽ™ā¯āŽ•āŽŗā¯ˆ āŽĩā¯†āŽŗāŽŋāŽ¯ā¯‡āŽąā¯āŽąā¯āŽŽā¯.", + "reset_sqlite_confirmation_note": "āŽ•ā¯āŽąāŽŋāŽĒā¯āŽĒ❁: āŽ¨ā¯€āŽ•ā¯āŽ•āŽŋāŽ¯ āŽĒāŽŋāŽąāŽ•ā¯, āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯āŽŸā¯ˆ āŽŽāŽąā¯āŽ¤ā¯ŠāŽŸāŽ•ā¯āŽ•āŽŽā¯ āŽšā¯†āŽ¯ā¯āŽ¯ āŽĩā¯‡āŽŖā¯āŽŸā¯āŽŽā¯.", + "reset_sqlite_done": "āŽ†āŽĒā¯āŽšā¯ āŽ¤āŽ°āŽĩ❁ āŽ…āŽ´āŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯. āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯āŽšā¯ˆ āŽŽāŽąā¯āŽ¤ā¯ŠāŽŸāŽ•ā¯āŽ•āŽŽā¯ āŽšā¯†āŽ¯ā¯āŽ¤ā¯ āŽŽā¯€āŽŖā¯āŽŸā¯āŽŽā¯ āŽ‰āŽŗā¯āŽ¨ā¯āŽ´ā¯ˆāŽ¯āŽĩā¯āŽŽā¯.", "reset_sqlite_success": "SQLITE āŽ¤āŽ°āŽĩā¯āŽ¤ā¯āŽ¤āŽŗāŽ¤ā¯āŽ¤ā¯ˆ āŽĩā¯†āŽąā¯āŽąāŽŋāŽ•āŽ°āŽŽāŽžāŽ• āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "reset_to_default": "āŽ‡āŽ¯āŽ˛ā¯āŽĒā¯āŽ¨āŽŋāŽ˛ā¯ˆāŽ•ā¯āŽ•ā¯ āŽŽā¯€āŽŸā¯āŽŸāŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "resolution": "āŽ¤ā¯†āŽŗāŽŋāŽĩā¯āŽ¤ā¯āŽ¤āŽŋāŽąāŽŠā¯", @@ -1764,9 +1911,12 @@ "saved_settings": "āŽšā¯‡āŽŽāŽŋāŽ¤ā¯āŽ¤ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯", "say_something": "āŽāŽ¤āŽžāŽĩāŽ¤ā¯ āŽšā¯ŠāŽ˛ā¯āŽ˛ā¯āŽ™ā¯āŽ•āŽŗā¯", "scaffold_body_error_occurred": "āŽĒāŽŋāŽ´ā¯ˆ āŽāŽąā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", + "scaffold_body_error_unrecoverable": "āŽŽā¯€āŽŸā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽ¤ āŽĒāŽŋāŽ´ā¯ˆ āŽāŽąā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯. āŽŸāŽŋāŽšā¯āŽ•āŽžāŽ°ā¯āŽŸā¯ āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽ•āŽŋāŽŸā¯āŽ…āŽĒā¯āŽĒāŽŋāŽ˛ā¯ āŽĒāŽŋāŽ´ā¯ˆ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽ…āŽŸā¯āŽ•ā¯āŽ•ā¯ āŽŸā¯āŽ°ā¯‡āŽšā¯ˆāŽĒā¯ āŽĒāŽ•āŽŋāŽ°āŽĩā¯āŽŽā¯, āŽ…āŽ¤āŽŠāŽžāŽ˛ā¯ āŽ¨āŽžāŽ™ā¯āŽ•āŽŗā¯ āŽ‰āŽ¤āŽĩ āŽŽā¯āŽŸāŽŋāŽ¯ā¯āŽŽā¯. āŽ…āŽąāŽŋāŽĩā¯āŽąā¯āŽ¤ā¯āŽ¤āŽĒā¯āŽĒāŽŸā¯āŽŸāŽžāŽ˛ā¯, āŽ•ā¯€āŽ´ā¯‡ āŽ‰āŽŗā¯āŽŗ āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯āŽŸā¯āŽ¤ā¯ āŽ¤āŽ°āŽĩ❈ āŽ…āŽ´āŽŋāŽ•ā¯āŽ•āŽ˛āŽžāŽŽā¯.", + "scan": "āŽĩāŽ°ā¯āŽŸā¯ āŽšā¯†āŽ¯ā¯āŽ¯āŽĩā¯āŽŽā¯", "scan_all_libraries": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽ¨ā¯‚āŽ˛āŽ•āŽ™ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽšā¯āŽ•ā¯‡āŽŠā¯ āŽšā¯†āŽ¯ā¯āŽ¯ā¯āŽ™ā¯āŽ•āŽŗā¯", "scan_library": "āŽšā¯āŽ•ā¯‡āŽŠā¯", "scan_settings": "āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽšā¯āŽ•ā¯‡āŽŠā¯ āŽšā¯†āŽ¯ā¯āŽ¯ā¯āŽ™ā¯āŽ•āŽŗā¯", + "scanning": "āŽĩāŽ°ā¯āŽŸā¯ āŽšā¯†āŽ¯ā¯āŽ•āŽŋāŽąāŽ¤ā¯", "scanning_for_album": "āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤āŽŋāŽąā¯āŽ•ā¯ āŽšā¯āŽ•ā¯‡āŽŠāŽŋāŽ™ā¯ ...", "search": "āŽ¤ā¯‡āŽŸāŽ˛ā¯", "search_albums": "āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽŸā¯āŽ™ā¯āŽ•āŽŗā¯", @@ -1796,6 +1946,8 @@ "search_filter_media_type_title": "āŽŽā¯€āŽŸāŽŋāŽ¯āŽž āŽĩāŽ•ā¯ˆāŽ¯ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "search_filter_ocr": "āŽ“āŽšāŽŋāŽ†āŽ°ā¯ āŽŽā¯‚āŽ˛āŽŽā¯ āŽ¤ā¯‡āŽŸā¯", "search_filter_people_title": "āŽŽāŽ•ā¯āŽ•āŽŗā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "search_filter_star_rating": "āŽ¨āŽŸā¯āŽšāŽ¤ā¯āŽ¤āŽŋāŽ° āŽŽāŽ¤āŽŋāŽĒā¯āŽĒā¯€āŽŸā¯", + "search_filter_tags_title": "āŽ•ā¯āŽąāŽŋāŽšā¯āŽšā¯ŠāŽąā¯āŽ•āŽŗā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "search_for": "āŽ¤ā¯‡āŽŸā¯āŽ™ā¯āŽ•āŽŗā¯", "search_for_existing_person": "āŽ‡āŽ°ā¯āŽ•ā¯āŽ•ā¯āŽŽā¯ āŽ¨āŽĒāŽ°ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽŸā¯āŽ™ā¯āŽ•āŽŗā¯", "search_no_more_result": "āŽŽā¯‡āŽ˛ā¯āŽŽā¯ āŽŽā¯āŽŸāŽŋāŽĩā¯āŽ•āŽŗā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ", @@ -1830,17 +1982,23 @@ "second": "āŽ‡āŽ°āŽŖā¯āŽŸāŽžāŽĩāŽ¤ā¯", "see_all_people": "āŽŽāŽ˛ā¯āŽ˛āŽž āŽŽāŽ•ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽĒāŽžāŽ°ā¯āŽ™ā¯āŽ•āŽŗā¯", "select": "āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯", + "select_album": "āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "select_album_cover": "āŽ†āŽ˛ā¯āŽĒāŽŽā¯ āŽ…āŽŸā¯āŽŸā¯ˆāŽ¯ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "select_albums": "āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "select_all": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ˆāŽ¯ā¯āŽŽā¯ āŽ¤ā¯†āŽ°āŽŋāŽĩā¯āŽšā¯†āŽ¯ā¯", "select_all_duplicates": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽ¨āŽ•āŽ˛ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "select_all_in": "{group} āŽ‡āŽ˛ā¯ āŽ‰āŽŗā¯āŽŗ āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ˆāŽ¯ā¯āŽŽā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "select_avatar_color": "āŽ…āŽĩāŽ¤āŽžāŽ°ā¯ āŽ¨āŽŋāŽąāŽ¤ā¯āŽ¤ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "select_count": "{count, plural, one {āŽ¤ā¯‡āŽ°ā¯āŽĩ❁ #} other {āŽ¤ā¯‡āŽ°ā¯āŽĩā¯āŽ•āŽŗā¯ #}}", + "select_cutoff_date": "āŽĩā¯†āŽŸā¯āŽŸā¯ āŽ¤ā¯‡āŽ¤āŽŋāŽ¯ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "select_face": "āŽŽā¯āŽ•āŽ¤ā¯āŽ¤ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "select_featured_photo": "āŽĒāŽŋāŽ°āŽ¤ā¯āŽ¯ā¯‡āŽ• āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ¤ā¯āŽ¤ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "select_from_computer": "āŽ•āŽŖāŽŋāŽŠāŽŋāŽ¯āŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "select_keep_all": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ˆāŽ¯ā¯āŽŽā¯ āŽĩā¯ˆāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽŠā¯āŽĒāŽ¤ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "select_library_owner": "āŽ¨ā¯‚āŽ˛āŽ• āŽ‰āŽ°āŽŋāŽŽā¯ˆāŽ¯āŽžāŽŗāŽ°ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "select_new_face": "āŽĒā¯āŽ¤āŽŋāŽ¯ āŽŽā¯āŽ•āŽ¤ā¯āŽ¤ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "select_people": "āŽ¨āŽĒāŽ°ā¯āŽ•āŽŗā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "select_person": "āŽ¨āŽĒāŽ°ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "select_person_to_tag": "āŽ•ā¯āŽąāŽŋāŽ•ā¯āŽ• āŽ’āŽ°ā¯ āŽ¨āŽĒāŽ°ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "select_photos": "āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "select_trash_all": "āŽ•ā¯āŽĒā¯āŽĒā¯ˆāŽ¤ā¯ āŽ¤ā¯ŠāŽŸā¯āŽŸāŽŋāŽ¯ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -1869,6 +2027,9 @@ "set_profile_picture": "āŽšā¯āŽ¯āŽĩāŽŋāŽĩāŽ°āŽĒā¯ āŽĒāŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽ…āŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "set_slideshow_to_fullscreen": "āŽšā¯āŽ˛ā¯ˆāŽŸā¯āŽšā¯‹āŽĩ❈ āŽŽā¯āŽ´ā¯āŽŽā¯ˆāŽ•ā¯āŽ•ā¯ āŽ…āŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "set_stack_primary_asset": "āŽŽā¯āŽ¤āŽŠā¯āŽŽā¯ˆ āŽšā¯ŠāŽ¤ā¯āŽ¤āŽžāŽ• āŽ…āŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "setting_image_navigation_enable_subtitle": "āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŋāŽ°ā¯āŽ¨ā¯āŽ¤āŽžāŽ˛ā¯, āŽ¤āŽŋāŽ°ā¯ˆāŽ¯āŽŋāŽŠā¯ āŽ‡āŽŸāŽ¤ā¯āŽĒā¯āŽąāŽŽā¯/āŽĩāŽ˛āŽ¤ā¯āŽĒā¯āŽąāŽŽā¯ āŽ‰āŽŗā¯āŽŗ āŽ•āŽžāŽ˛ā¯āŽĒāŽ•ā¯āŽ¤āŽŋāŽ¯ā¯ˆāŽ¤ā¯ āŽ¤āŽŸā¯āŽŸā¯āŽĩāŽ¤āŽŠā¯ āŽŽā¯‚āŽ˛āŽŽā¯ āŽŽā¯āŽ¨ā¯āŽ¤ā¯ˆāŽ¯/āŽ…āŽŸā¯āŽ¤ā¯āŽ¤ āŽĒāŽŸāŽ¤ā¯āŽ¤āŽŋāŽąā¯āŽ•ā¯āŽšā¯ āŽšā¯†āŽ˛ā¯āŽ˛āŽ˛āŽžāŽŽā¯.", + "setting_image_navigation_enable_title": "āŽĩāŽ´āŽŋāŽšā¯†āŽ˛ā¯āŽ¤ā¯āŽ¤ āŽ¤āŽŸā¯āŽŸāŽĩā¯āŽŽā¯", + "setting_image_navigation_title": "āŽĒāŽŸ āŽĩāŽ´āŽŋāŽšā¯†āŽ˛ā¯āŽ¤ā¯āŽ¤āŽ˛ā¯", "setting_image_viewer_help": "āŽĩāŽŋāŽĩāŽ°āŽŽā¯ āŽĒāŽžāŽ°ā¯āŽĩā¯ˆāŽ¯āŽžāŽŗāŽ°ā¯ āŽŽā¯āŽ¤āŽ˛āŽŋāŽ˛ā¯ āŽšāŽŋāŽąāŽŋāŽ¯ āŽšāŽŋāŽąā¯ āŽ‰āŽ°ā¯āŽĩāŽ¤ā¯āŽ¤ā¯ˆ āŽāŽąā¯āŽąā¯āŽ•āŽŋāŽąāŽžāŽ°ā¯, āŽĒāŽŋāŽŠā¯āŽŠāŽ°ā¯ āŽ¨āŽŸā¯āŽ¤ā¯āŽ¤āŽ° āŽ…āŽŗāŽĩāŽŋāŽ˛āŽžāŽŠ āŽŽā¯āŽŠā¯āŽŠā¯‹āŽŸā¯āŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽāŽąā¯āŽąā¯āŽ•āŽŋāŽąāŽžāŽ°ā¯ (āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽžāŽ˛ā¯), āŽ‡āŽąā¯āŽ¤āŽŋāŽ¯āŽžāŽ• āŽ…āŽšāŽ˛ā¯ˆ āŽāŽąā¯āŽąā¯āŽ•āŽŋāŽąāŽ¤ā¯ (āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽžāŽ˛ā¯).", "setting_image_viewer_original_subtitle": "āŽ…āŽšāŽ˛ā¯ āŽŽā¯āŽ´ā¯ āŽ¤ā¯†āŽŗāŽŋāŽĩā¯āŽ¤ā¯āŽ¤āŽŋāŽąāŽŠā¯ āŽĒāŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽāŽąā¯āŽąāŽĩā¯āŽŽā¯ (āŽĒā¯†āŽ°āŽŋāŽ¯āŽ¤ā¯!). āŽ¤āŽ°āŽĩ❁ āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯āŽŸā¯ˆāŽ•ā¯ āŽ•ā¯āŽąā¯ˆāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽ•ā¯āŽ•ā¯ (āŽĒāŽŋāŽŖā¯ˆāŽ¯āŽŽā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽšāŽžāŽ¤āŽŠ āŽ¤āŽąā¯āŽ•āŽžāŽ˛āŽŋāŽ• āŽšā¯‡āŽŽāŽŋāŽĒā¯āŽĒ❁ āŽ‡āŽ°āŽŖā¯āŽŸā¯āŽŽā¯).", "setting_image_viewer_original_title": "āŽ…āŽšāŽ˛ā¯ āŽĒāŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽāŽąā¯āŽąāŽĩā¯āŽŽā¯", @@ -1976,6 +2137,7 @@ "show_password": "āŽ•āŽŸāŽĩā¯āŽšā¯āŽšā¯ŠāŽ˛ā¯āŽ˛ā¯ˆāŽ•ā¯ āŽ•āŽžāŽŸā¯āŽŸā¯", "show_person_options": "āŽ¨āŽĒāŽ°ā¯ āŽĩāŽŋāŽ°ā¯āŽĒā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ˆāŽ•ā¯ āŽ•āŽžāŽŸā¯āŽŸā¯", "show_progress_bar": "āŽŽā¯āŽŠā¯āŽŠā¯‡āŽąā¯āŽąāŽĒā¯ āŽĒāŽŸā¯āŽŸāŽŋāŽ¯ā¯ˆāŽ•ā¯ āŽ•āŽžāŽŸā¯āŽŸā¯", + "show_schema": "āŽ¤āŽŋāŽŸā¯āŽŸāŽ¤ā¯āŽ¤ā¯ˆāŽ•ā¯ āŽ•āŽžāŽŸā¯āŽŸā¯", "show_search_options": "āŽ¤ā¯‡āŽŸāŽ˛ā¯ āŽĩāŽŋāŽ°ā¯āŽĒā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ˆāŽ•ā¯ āŽ•āŽžāŽŸā¯āŽŸā¯", "show_shared_links": "āŽĒāŽ•āŽŋāŽ°āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ‡āŽŖā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆāŽ•ā¯ āŽ•āŽžāŽŸā¯āŽŸā¯", "show_slideshow_transition": "āŽšā¯āŽ˛ā¯ˆāŽŸā¯āŽšā¯‹ āŽŽāŽžāŽąā¯āŽąāŽ¤ā¯āŽ¤ā¯ˆāŽ•ā¯ āŽ•āŽžāŽŸā¯āŽŸā¯", @@ -1993,6 +2155,8 @@ "skip_to_folders": "āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽąā¯ˆāŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯āŽšā¯ āŽšā¯†āŽ˛ā¯āŽ˛ā¯āŽ™ā¯āŽ•āŽŗā¯", "skip_to_tags": "āŽ•ā¯āŽąāŽŋāŽšā¯āŽšā¯ŠāŽąā¯āŽ•āŽŗā¯ˆāŽ¤ā¯ āŽ¤āŽĩāŽŋāŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "slideshow": "āŽšā¯āŽ˛ā¯ˆāŽŸā¯āŽšā¯‹", + "slideshow_repeat": "āŽšā¯āŽ˛ā¯ˆāŽŸā¯āŽšā¯‹āŽĩ❈ āŽŽā¯€āŽŖā¯āŽŸā¯āŽŽā¯ āŽšā¯†āŽ¯ā¯āŽ¯āŽĩā¯āŽŽā¯", + "slideshow_repeat_description": "āŽšā¯āŽ˛ā¯ˆāŽŸā¯āŽšā¯‹ āŽŽā¯āŽŸāŽŋāŽĩāŽŸā¯ˆāŽ¯ā¯āŽŽā¯ āŽĒā¯‹āŽ¤ā¯ āŽŽā¯€āŽŖā¯āŽŸā¯āŽŽā¯ āŽ¤ā¯ŠāŽŸāŽ•ā¯āŽ•āŽ¤ā¯āŽ¤āŽŋāŽąā¯āŽ•ā¯āŽšā¯ āŽšā¯†āŽ˛ā¯āŽ˛āŽĩā¯āŽŽā¯", "slideshow_settings": "āŽšā¯āŽ˛ā¯ˆāŽŸā¯āŽšā¯‹ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯", "sort_albums_by": "āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ˆ āŽĩāŽ°āŽŋāŽšā¯ˆāŽĒā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤ā¯āŽ™ā¯āŽ•āŽŗā¯ ...", "sort_created": "āŽ¤ā¯‡āŽ¤āŽŋ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", @@ -2032,6 +2196,7 @@ "support": "āŽ‰āŽ¤āŽĩāŽŋ", "support_and_feedback": "āŽ‰āŽ¤āŽĩāŽŋ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤ā¯", "support_third_party_description": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯ āŽ¨āŽŋāŽąā¯āŽĩāŽ˛ā¯ āŽŽā¯‚āŽŠā¯āŽąāŽžāŽŽā¯ āŽ¤āŽ°āŽĒā¯āŽĒāŽŋāŽŠāŽ°āŽžāŽ˛ā¯ āŽ¤ā¯ŠāŽ•ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯. āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽ…āŽŠā¯āŽĒāŽĩāŽŋāŽ•ā¯āŽ•ā¯āŽŽā¯ āŽšāŽŋāŽ•ā¯āŽ•āŽ˛ā¯āŽ•āŽŗā¯ āŽ…āŽ¨ā¯āŽ¤ āŽ¤ā¯ŠāŽ•ā¯āŽĒā¯āŽĒāŽžāŽ˛ā¯ āŽāŽąā¯āŽĒāŽŸāŽ˛āŽžāŽŽā¯, āŽŽāŽŠāŽĩ❇ āŽ•ā¯€āŽ´ā¯‡āŽ¯ā¯āŽŗā¯āŽŗ āŽ‡āŽŖā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽŋ āŽŽā¯āŽ¤āŽ˛ā¯ āŽšāŽ¨ā¯āŽ¤āŽ°ā¯āŽĒā¯āŽĒāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽ…āŽĩāŽ°ā¯āŽ•āŽŗā¯āŽŸāŽŠā¯ āŽšāŽŋāŽ•ā¯āŽ•āŽ˛ā¯āŽ•āŽŗā¯ˆ āŽŽāŽ´ā¯āŽĒā¯āŽĒā¯āŽ™ā¯āŽ•āŽŗā¯.", + "supporter": "āŽ†āŽ¤āŽ°āŽĩāŽžāŽŗāŽ°ā¯", "swap_merge_direction": "āŽ’āŽŠā¯āŽąāŽŋāŽŖā¯ˆāŽ•ā¯āŽ•ā¯āŽŽā¯ āŽ¤āŽŋāŽšā¯ˆāŽ¯ā¯ˆ āŽŽāŽžāŽąā¯āŽąāŽĩā¯āŽŽā¯", "sync": "āŽ’āŽ¤ā¯āŽ¤āŽŋāŽšā¯ˆāŽĩ❁", "sync_albums": "āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯ˆ āŽ’āŽ¤ā¯āŽ¤āŽŋāŽšā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -2069,6 +2234,7 @@ "theme_setting_theme_subtitle": "āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯āŽŸāŽŋāŽŠā¯ āŽ•āŽ°ā¯āŽĒā¯āŽĒā¯ŠāŽ°ā¯āŽŗā¯ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽĩā¯āŽšā¯†āŽ¯ā¯āŽ•", "theme_setting_three_stage_loading_subtitle": "āŽŽā¯‚āŽŠā¯āŽąā¯-āŽ¨āŽŋāŽ˛ā¯ˆ āŽāŽąā¯āŽąā¯āŽ¤āŽ˛ā¯ āŽ‡āŽ¯āŽ•ā¯āŽ•āŽŋāŽŠāŽžāŽ˛ā¯ āŽāŽąā¯āŽąā¯āŽ¤āŽ˛ā¯ āŽšā¯†āŽ¯āŽ˛ā¯āŽ¤āŽŋāŽąāŽŠā¯ˆ āŽ…āŽ¤āŽŋāŽ•āŽ°āŽŋāŽ•ā¯āŽ•āŽ•ā¯āŽ•ā¯‚āŽŸā¯āŽŽā¯, āŽ†āŽŠāŽžāŽ˛ā¯ āŽ•āŽŖāŽŋāŽšāŽŽāŽžāŽ• āŽŽāŽŋāŽ•ā¯ˆ āŽĒāŽŋāŽŖā¯ˆāŽ¯āŽšā¯ āŽšā¯āŽŽā¯ˆāŽ¯ā¯ˆ āŽāŽąā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤ā¯āŽ•āŽŋāŽąāŽ¤ā¯", "theme_setting_three_stage_loading_title": "āŽŽā¯‚āŽŠā¯āŽąā¯-āŽ¨āŽŋāŽ˛ā¯ˆ āŽāŽąā¯āŽąā¯āŽ¤āŽ˛ā¯ˆ āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "then": "āŽĒāŽŋāŽąāŽ•ā¯", "they_will_be_merged_together": "āŽ…āŽĩāŽ°ā¯āŽ•āŽŗā¯ āŽ’āŽŠā¯āŽąāŽžāŽ• āŽ‡āŽŖā¯ˆāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽĩāŽžāŽ°ā¯āŽ•āŽŗā¯", "third_party_resources": "āŽŽā¯‚āŽŠā¯āŽąāŽžāŽŽā¯ āŽ¤āŽ°āŽĒā¯āŽĒ❁ āŽĩāŽŗāŽ™ā¯āŽ•āŽŗā¯", "time": "āŽ¨ā¯‡āŽ°āŽŽā¯", @@ -2103,6 +2269,13 @@ "trash_page_select_assets_btn": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "trash_page_title": "({count})", "trashed_items_will_be_permanently_deleted_after": "āŽ•ā¯āŽĒā¯āŽĒā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽ‰āŽŗā¯āŽŗ āŽ‰āŽ°ā¯āŽĒā¯āŽĒāŽŸāŽŋāŽ•āŽŗā¯ {days, plural, one {# āŽ¨āŽžāŽŗā¯āŽ•ā¯āŽ•ā¯} other {# āŽ¨āŽžāŽŸā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯}}āŽĒāŽŋāŽąāŽ•ā¯ āŽ¨āŽŋāŽ°āŽ¨ā¯āŽ¤āŽ°āŽŽāŽžāŽ• āŽ¨ā¯€āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯.", + "trigger": "āŽ¤ā¯‚āŽŖā¯āŽŸā¯āŽ¤āŽ˛ā¯", + "trigger_asset_uploaded": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", + "trigger_asset_uploaded_description": "āŽĒā¯āŽ¤āŽŋāŽ¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽĒā¯āŽĒāŽŸā¯āŽŽā¯ āŽĒā¯‹āŽ¤ā¯ āŽ¤ā¯‚āŽŖā¯āŽŸāŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", + "trigger_description": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩ❁ āŽ¤ā¯ŠāŽŸāŽ™ā¯āŽ•ā¯āŽŽā¯ āŽ’āŽ°ā¯ āŽ¨āŽŋāŽ•āŽ´ā¯āŽĩ❁", + "trigger_person_recognized": "āŽ…āŽŸā¯ˆāŽ¯āŽžāŽŗāŽŽā¯ āŽ•āŽžāŽŖāŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ¨āŽĒāŽ°ā¯", + "trigger_person_recognized_description": "āŽ’āŽ°ā¯ āŽ¨āŽĒāŽ°ā¯ āŽ•āŽŖā¯āŽŸāŽąāŽŋāŽ¯āŽĒā¯āŽĒāŽŸā¯āŽŸāŽžāŽ˛ā¯ āŽ¤ā¯‚āŽŖā¯āŽŸāŽĒā¯āŽĒāŽŸā¯āŽ•āŽŋāŽąāŽ¤ā¯", + "trigger_type": "āŽ¤ā¯‚āŽŖā¯āŽŸā¯āŽ¤āŽ˛ā¯ āŽĩāŽ•ā¯ˆ", "troubleshoot": "āŽšāŽ°āŽŋāŽšā¯†āŽ¯ā¯āŽ¤āŽ˛ā¯", "type": "āŽĩāŽ•ā¯ˆ", "unable_to_change_pin_code": "āŽŽā¯āŽŗā¯ āŽ•ā¯āŽąāŽŋāŽ¯ā¯€āŽŸā¯āŽŸā¯ˆ āŽŽāŽžāŽąā¯āŽą āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", @@ -2117,6 +2290,7 @@ "unhide_person": "āŽ…āŽ°ā¯āŽĩāŽ°ā¯āŽĒā¯āŽĒāŽžāŽŠ āŽ¨āŽĒāŽ°ā¯", "unknown": "āŽ¤ā¯†āŽ°āŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unknown_country": "āŽ¤ā¯†āŽ°āŽŋāŽ¯āŽžāŽ¤ āŽ¨āŽžāŽŸā¯", + "unknown_date": "āŽ¤ā¯†āŽ°āŽŋāŽ¯āŽžāŽ¤ āŽ¤ā¯‡āŽ¤āŽŋ", "unknown_year": "āŽ¤ā¯†āŽ°āŽŋāŽ¯āŽžāŽ¤ āŽ†āŽŖā¯āŽŸā¯", "unlimited": "āŽĩāŽ°āŽŽā¯āŽĒāŽąā¯āŽąāŽ¤ā¯", "unlink_motion_video": "āŽ‡āŽ¯āŽ•ā¯āŽ• āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽĩ❈ āŽ‡āŽŖā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -2133,7 +2307,10 @@ "unstack": "āŽ…āŽŠā¯-āŽšā¯āŽŸāŽžāŽ•ā¯", "unstack_action_prompt": "{count} āŽ¤āŽŸā¯ˆāŽ¯āŽŋāŽŠā¯āŽąāŽŋ", "unstacked_assets_count": "āŽ…āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽžāŽ¤ {count, plural, one {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯} other {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯}}", + "unsupported_field_type": "āŽ†āŽ¤āŽ°āŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽžāŽ¤ āŽĒā¯āŽ˛ āŽĩāŽ•ā¯ˆ", + "unsupported_file_type": "āŽ•ā¯‹āŽĒā¯āŽĒ❈ {file} āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽą āŽŽā¯āŽŸāŽŋāŽ¯āŽžāŽ¤ā¯, āŽāŽŠā¯†āŽŠāŽŋāŽ˛ā¯ āŽ…āŽ¤āŽŠā¯ āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽĩāŽ•ā¯ˆ {type} āŽ†āŽ¤āŽ°āŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ.", "untagged": "āŽ…āŽĩāŽŋāŽ´ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽžāŽ¤āŽ¤ā¯", + "untitled_workflow": "āŽĒā¯†āŽ¯āŽ°āŽŋāŽŸāŽĒā¯āŽĒāŽŸāŽžāŽ¤ āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩ❁", "up_next": "āŽ…āŽŸā¯āŽ¤ā¯āŽ¤ā¯", "update_location_action_prompt": "{count} āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗāŽŋāŽŠā¯ āŽ‡āŽ°ā¯āŽĒā¯āŽĒāŽŋāŽŸāŽ¤ā¯āŽ¤ā¯ˆāŽĒā¯ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯:", "updated_at": "āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", @@ -2143,6 +2320,7 @@ "upload_details": "āŽĩāŽŋāŽĩāŽ°āŽ™ā¯āŽ•āŽŗā¯ˆ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽĩā¯āŽŽā¯", "upload_dialog_info": "āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ (āŽ•āŽŗā¯ˆ) āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ•āŽ¤ā¯āŽ¤āŽŋāŽąā¯āŽ•ā¯ āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯ āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽŽāŽŸā¯āŽ•ā¯āŽ• āŽĩāŽŋāŽ°ā¯āŽŽā¯āŽĒā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗāŽž?", "upload_dialog_title": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ˆ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽĩā¯āŽŽā¯", + "upload_error_with_count": "{count, plural, one {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•ā¯} other {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯}} āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽĒā¯ āŽĒāŽŋāŽ´ā¯ˆ", "upload_errors": "{count, plural, one {# āŽĒāŽŋāŽ´ā¯ˆ} other {# āŽĒāŽŋāŽ´ā¯ˆāŽ•āŽŗā¯}}āŽŽā¯‚āŽ˛āŽŽā¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽŽā¯ āŽŽā¯āŽŸāŽŋāŽ¨ā¯āŽ¤āŽ¤ā¯, āŽĒā¯āŽ¤āŽŋāŽ¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽą āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•āŽŗā¯ˆāŽĒā¯ āŽĒāŽžāŽ°ā¯āŽ•ā¯āŽ•āŽĒā¯ āŽĒāŽ•ā¯āŽ•āŽ¤ā¯āŽ¤ā¯ˆāŽĒā¯ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯.", "upload_finished": "āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽŽā¯ āŽŽā¯āŽŸāŽŋāŽ¨ā¯āŽ¤āŽ¤ā¯", "upload_progress": "āŽŽā¯€āŽ¤āŽŽā¯āŽŗā¯āŽŗ {remaining, number} - āŽšā¯†āŽ¯āŽ˛āŽžāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯ {processed, number}/{total, number}", @@ -2157,6 +2335,8 @@ "url": "āŽŽā¯āŽ•āŽĩāŽ°āŽŋ", "usage": "āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯", "use_biometric": "āŽĒāŽ¯ā¯‹āŽŽā¯†āŽŸā¯āŽ°āŽŋāŽ•ā¯āŽ•ā¯ˆāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", + "use_browser_locale": "āŽ‰āŽ˛āŽžāŽĩāŽŋāŽ¯āŽŋāŽŠā¯ āŽŽā¯ŠāŽ´āŽŋāŽ¯ā¯ˆāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", + "use_browser_locale_description": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ‰āŽ˛āŽžāŽĩāŽŋāŽ¯āŽŋāŽŠā¯ āŽ‡āŽ°ā¯āŽĒā¯āŽĒāŽŋāŽŸāŽ¤ā¯āŽ¤āŽŋāŽŠā¯ āŽ…āŽŸāŽŋāŽĒā¯āŽĒāŽŸā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽ¤ā¯‡āŽ¤āŽŋāŽ•āŽŗā¯, āŽ¨ā¯‡āŽ°āŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽŽāŽŖā¯āŽ•āŽŗā¯ˆ āŽĩāŽŸāŽŋāŽĩāŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "use_current_connection": "āŽ¤āŽąā¯āŽĒā¯‹āŽ¤ā¯ˆāŽ¯ āŽ‡āŽŖā¯ˆāŽĒā¯āŽĒ❈āŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", "use_custom_date_range": "āŽ…āŽ¤āŽąā¯āŽ•ā¯ āŽĒāŽ¤āŽŋāŽ˛āŽžāŽ• āŽ¤āŽŠāŽŋāŽĒā¯āŽĒāŽ¯āŽŠā¯ āŽ¤ā¯‡āŽ¤āŽŋ āŽĩāŽ°āŽŽā¯āŽĒ❈āŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", "user": "āŽĒāŽ¯āŽŠāŽ°ā¯", @@ -2178,10 +2358,11 @@ "utilities": "āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯āŽ•āŽŗā¯", "validate": "āŽšāŽ°āŽŋāŽĒāŽžāŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "validate_endpoint_error": "āŽ¤āŽ¯āŽĩā¯āŽšā¯†āŽ¯ā¯āŽ¤ā¯ āŽ’āŽ°ā¯ āŽšā¯†āŽ˛ā¯āŽ˛ā¯āŽĒāŽŸāŽŋāŽ¯āŽžāŽ•ā¯āŽŽā¯ URL āŽ āŽ‰āŽŗā¯āŽŗāŽŋāŽŸāŽĩā¯āŽŽā¯", + "validation_error": "āŽšāŽ°āŽŋāŽĒāŽžāŽ°ā¯āŽĒā¯āŽĒ❁ āŽĒāŽŋāŽ´ā¯ˆ", "variables": "āŽŽāŽžāŽąāŽŋāŽ•āŽŗā¯", "version": "āŽĒāŽ¤āŽŋāŽĒā¯āŽĒ❁", "version_announcement_closing": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ¨āŽŖā¯āŽĒāŽ°ā¯, āŽ…āŽ˛ā¯†āŽ•ā¯āŽšā¯", - "version_announcement_message": "āŽĩāŽŖāŽ•ā¯āŽ•āŽŽā¯! āŽ‡āŽŽā¯āŽŽāŽŋāŽ¯āŽŋāŽŠā¯ āŽĒā¯āŽ¤āŽŋāŽ¯ āŽĒāŽ¤āŽŋāŽĒā¯āŽĒ❁ āŽ•āŽŋāŽŸā¯ˆāŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯. āŽŽāŽ¨ā¯āŽ¤āŽĩā¯ŠāŽ°ā¯ āŽ¤āŽĩāŽąāŽžāŽŠ āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽ¤āŽŸā¯āŽ•ā¯āŽ• āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒ❁ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ¤ā¯āŽ¤ āŽ¨āŽŋāŽ˛ā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽ‡āŽ°ā¯āŽĒā¯āŽĒāŽ¤ā¯ˆ āŽ‰āŽąā¯āŽ¤āŽŋāŽšā¯†āŽ¯ā¯āŽ¯ āŽĩā¯†āŽŗāŽŋāŽ¯ā¯€āŽŸā¯āŽŸā¯āŽ•ā¯ āŽ•ā¯āŽąāŽŋāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ āŽāŽĒā¯ āŽĒāŽŸāŽŋāŽ•ā¯āŽ• āŽšāŽŋāŽąāŽŋāŽ¤ā¯ āŽ¨ā¯‡āŽ°āŽŽā¯ āŽ’āŽ¤ā¯āŽ•ā¯āŽ•ā¯āŽ™ā¯āŽ•āŽŗā¯, āŽ•ā¯āŽąāŽŋāŽĒā¯āŽĒāŽžāŽ• āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽ•āŽžāŽĩāŽąā¯āŽ•ā¯‹āŽĒā¯āŽ°āŽ¤ā¯āŽ¤ā¯ˆāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽŋāŽŠāŽžāŽ˛ā¯ āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯ āŽ¨āŽŋāŽ•āŽ´ā¯āŽĩ❈ āŽ¤āŽžāŽŠāŽžāŽ•āŽĩ❇ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽĒā¯āŽĒāŽ¤ā¯ˆāŽ•ā¯ āŽ•ā¯ˆāŽ¯āŽžāŽŗā¯āŽŽā¯ āŽŽāŽ¨ā¯āŽ¤āŽĩā¯ŠāŽ°ā¯ āŽĒā¯ŠāŽąāŽŋāŽŽā¯āŽąā¯ˆāŽ¯ā¯ˆāŽ¯ā¯āŽŽā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽŋāŽŠāŽžāŽ˛ā¯.", + "version_announcement_message": "āŽĩāŽŖāŽ•ā¯āŽ•āŽŽā¯! āŽ‡āŽŽā¯āŽŽāŽŋāŽ¯āŽŋāŽŠā¯ āŽĒā¯āŽ¤āŽŋāŽ¯ āŽĒāŽ¤āŽŋāŽĒā¯āŽĒ❁ āŽ•āŽŋāŽŸā¯ˆāŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯. āŽŽāŽ¨ā¯āŽ¤āŽĩā¯ŠāŽ°ā¯ āŽ¤āŽĩāŽąāŽžāŽŠ āŽ•āŽ°ā¯āŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽ¤āŽŸā¯āŽ•ā¯āŽ• āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒ❁ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ¤ā¯āŽ¤ āŽ¨āŽŋāŽ˛ā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽ‡āŽ°ā¯āŽĒā¯āŽĒāŽ¤ā¯ˆ āŽ‰āŽąā¯āŽ¤āŽŋāŽšā¯†āŽ¯ā¯āŽ¯ āŽĩā¯†āŽŗāŽŋāŽ¯ā¯€āŽŸā¯āŽŸā¯āŽ•ā¯ āŽ•ā¯āŽąāŽŋāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ āŽāŽĒā¯ āŽĒāŽŸāŽŋāŽ•ā¯āŽ•āŽšā¯ āŽšāŽŋāŽąāŽŋāŽ¤ā¯ āŽ¨ā¯‡āŽ°āŽŽā¯ āŽ’āŽ¤ā¯āŽ•ā¯āŽ•ā¯āŽ™ā¯āŽ•āŽŗā¯, āŽ•ā¯āŽąāŽŋāŽĒā¯āŽĒāŽžāŽ• āŽ¨ā¯€āŽ™ā¯āŽ•āŽŗā¯ āŽ•āŽžāŽĩāŽąā¯āŽ•ā¯‹āŽĒā¯āŽ°āŽ¤ā¯āŽ¤ā¯ˆāŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽŋāŽŠāŽžāŽ˛ā¯ āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯ āŽ¨āŽŋāŽ•āŽ´ā¯āŽĩā¯ˆāŽ¤ā¯ āŽ¤āŽžāŽŠāŽžāŽ•āŽĩ❇ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽĒā¯āŽĒāŽ¤ā¯ˆāŽ•ā¯ āŽ•ā¯ˆāŽ¯āŽžāŽŗā¯āŽŽā¯ āŽŽāŽ¨ā¯āŽ¤āŽĩā¯ŠāŽ°ā¯ āŽĒā¯ŠāŽąāŽŋāŽŽā¯āŽąā¯ˆāŽ¯ā¯ˆāŽ¯ā¯āŽŽā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽŋāŽŠāŽžāŽ˛ā¯.", "version_history": "āŽĒāŽ¤āŽŋāŽĒā¯āŽĒ❁ āŽĩāŽ°āŽ˛āŽžāŽąā¯", "version_history_item": "{version} āŽ‡āŽ˛ā¯ {date} āŽ¨āŽŋāŽąā¯āŽĩāŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "video": "āŽ’āŽŗāŽŋāŽ¤ā¯‹āŽąā¯āŽąāŽŽā¯", @@ -2189,6 +2370,7 @@ "video_hover_setting_description": "āŽŽāŽĩā¯āŽšā¯ āŽ‰āŽ°ā¯āŽĒā¯āŽĒāŽŸāŽŋāŽ¯ā¯ˆāŽ•ā¯ āŽ•ā¯ŠāŽŖā¯āŽŸā¯ āŽšā¯†āŽ˛ā¯āŽ˛ā¯āŽŽā¯āŽĒā¯‹āŽ¤ā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹ āŽšāŽŋāŽąā¯ āŽ‰āŽ°ā¯āŽĩāŽ¤ā¯āŽ¤ā¯ˆ āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯. āŽŽā¯āŽŸāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽžāŽ˛ā¯āŽŽā¯ āŽ•ā¯‚āŽŸ, āŽĒāŽŋāŽŗā¯‡ āŽāŽ•āŽžāŽŠā¯āŽ•ā¯āŽ•ā¯ āŽŽā¯‡āŽ˛ā¯ āŽšā¯āŽąā¯āŽąā¯āŽĩāŽ¤āŽŠā¯ āŽŽā¯‚āŽ˛āŽŽā¯ āŽĒāŽŋāŽŗā¯‡āŽĒā¯‡āŽ•ā¯āŽ•ā¯ˆāŽ¤ā¯ āŽ¤ā¯ŠāŽŸāŽ™ā¯āŽ•āŽ˛āŽžāŽŽā¯.", "videos": "āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯", "videos_count": "{count, plural, one {# āŽ•āŽžāŽŖā¯ŠāŽŗāŽŋ} other {# āŽ•āŽžāŽŖā¯ŠāŽŗāŽŋāŽ•āŽŗā¯}}", + "videos_only": "āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ āŽŽāŽŸā¯āŽŸā¯āŽŽā¯‡", "view": "āŽĒāŽžāŽ°ā¯āŽĩ❈", "view_album": "āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤ā¯ˆāŽ•ā¯ āŽ•āŽžāŽŖā¯āŽ•", "view_all": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ˆāŽ¯ā¯āŽŽā¯ āŽ•āŽžāŽŖā¯āŽ•", @@ -2209,18 +2391,36 @@ "viewer_stack_use_as_main_asset": "āŽĒāŽŋāŽ°āŽ¤āŽžāŽŠ āŽšā¯ŠāŽ¤ā¯āŽ¤āŽžāŽ•āŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", "viewer_unstack": "āŽ…āŽŸā¯āŽ•ā¯āŽ•ā¯ˆ āŽ¨ā¯€āŽ•ā¯āŽ•ā¯", "visibility_changed": "{count, plural, one {# āŽ¨āŽĒāŽ°ā¯} other {# āŽ¨āŽĒāŽ°ā¯āŽ•āŽŗā¯}} āŽ•ā¯āŽ•āŽžāŽŠ āŽ¤ā¯†āŽ°āŽŋāŽĩā¯āŽ¨āŽŋāŽ˛ā¯ˆ āŽŽāŽžāŽąā¯āŽąāŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", + "visual": "āŽ•āŽžāŽŸā¯āŽšāŽŋ", + "visual_builder": "āŽ•āŽžāŽŸā¯āŽšāŽŋ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•ā¯āŽĒāŽĩāŽ°ā¯", "waiting": "āŽ•āŽžāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯", + "waiting_count": "āŽ•āŽžāŽ¤ā¯āŽ¤āŽŋāŽ°ā¯āŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯: {count}", "warning": "āŽŽāŽšā¯āŽšāŽ°āŽŋāŽ•ā¯āŽ•ā¯ˆ", "week": "āŽĩāŽžāŽ°āŽŽā¯", "welcome": "āŽĩāŽ°āŽĩā¯‡āŽąā¯āŽ•āŽŋāŽąā¯‹āŽŽā¯", "welcome_to_immich": "āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯āŽšāŽŋāŽąā¯āŽ•ā¯ āŽĩāŽ°ā¯āŽ•", + "width": "āŽ…āŽ•āŽ˛āŽŽā¯", "wifi_name": "āŽĩā¯ˆāŽƒāŽĒ❈ āŽĒā¯†āŽ¯āŽ°ā¯", + "workflow_delete_prompt": "āŽ‡āŽ¨ā¯āŽ¤ āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩā¯āŽ•āŽŗā¯ˆ āŽ¨āŽŋāŽšā¯āŽšāŽ¯āŽŽāŽžāŽ• āŽ¨ā¯€āŽ•ā¯āŽ• āŽĩāŽŋāŽ°ā¯āŽŽā¯āŽĒā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗāŽž?", + "workflow_deleted": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩ❁ āŽ¨ā¯€āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", + "workflow_description": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩ❁ āŽĩāŽŋāŽŗāŽ•ā¯āŽ•āŽŽā¯", + "workflow_info": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩ❁ āŽšā¯†āŽ¯ā¯āŽ¤āŽŋ", + "workflow_json": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩ❁ āŽšāŽžāŽ¤ā¯ŠāŽĒā¯ŠāŽ•ā¯", + "workflow_json_help": "āŽšāŽžāŽ¤ā¯ŠāŽĒā¯ŠāŽ•ā¯ āŽĩāŽŸāŽŋāŽĩāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩ❁ āŽ‰āŽŗā¯āŽŗāŽŽā¯ˆāŽĩā¯ˆāŽ¤ā¯ āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯. āŽŽāŽžāŽąā¯āŽąāŽ™ā¯āŽ•āŽŗā¯ āŽ•āŽžāŽŸā¯āŽšāŽŋ āŽĒāŽŋāŽ˛ā¯āŽŸāŽ°ā¯āŽŸāŽŠā¯ āŽ’āŽ¤ā¯āŽ¤āŽŋāŽšā¯ˆāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯.", + "workflow_name": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩ❁ āŽĒā¯†āŽ¯āŽ°ā¯", + "workflow_navigation_prompt": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽŽāŽžāŽąā¯āŽąāŽ™ā¯āŽ•āŽŗā¯ˆāŽšā¯ āŽšā¯‡āŽŽāŽŋāŽ•ā¯āŽ•āŽžāŽŽāŽ˛ā¯ āŽ¨āŽŋāŽšā¯āŽšāŽ¯āŽŽāŽžāŽ• āŽĩā¯†āŽŗāŽŋāŽ¯ā¯‡āŽą āŽĩāŽŋāŽ°ā¯āŽŽā¯āŽĒā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗāŽž?", + "workflow_summary": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩ❁ āŽšā¯āŽ°ā¯āŽ•ā¯āŽ•āŽŽā¯", + "workflow_update_success": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩ❁ āŽĩā¯†āŽąā¯āŽąāŽŋāŽ•āŽ°āŽŽāŽžāŽ• āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", + "workflow_updated": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩ❁ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", + "workflows": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩā¯āŽ•āŽŗā¯", + "workflows_help_text": "āŽ¤ā¯‚āŽŖā¯āŽŸā¯āŽ¤āŽ˛ā¯āŽ•āŽŗā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽĩāŽŸāŽŋāŽĒā¯āŽĒāŽžāŽŠā¯āŽ•āŽŗāŽŋāŽŠā¯ āŽ…āŽŸāŽŋāŽĒā¯āŽĒāŽŸā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩā¯āŽ•āŽŗā¯ āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•āŽŗāŽŋāŽ˛ā¯ āŽšā¯†āŽ¯āŽ˛ā¯āŽ•āŽŗā¯ˆ āŽ¤āŽžāŽŠāŽŋāŽ¯āŽ™ā¯āŽ•ā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤ā¯āŽ•āŽŋāŽŠā¯āŽąāŽŠ", "wrong_pin_code": "āŽ¤āŽĩāŽąāŽžāŽŠ āŽĒāŽŋāŽŠā¯ āŽ•ā¯āŽąāŽŋāŽ¯ā¯€āŽŸā¯", "year": "āŽ†āŽŖā¯āŽŸā¯", "years_ago": "{years, plural, one {# āŽ†āŽŖā¯āŽŸā¯} other {# āŽ†āŽŖā¯āŽŸā¯āŽ•āŽŗā¯}} āŽŽā¯āŽŠā¯āŽĒ❁", "yes": "āŽ†āŽŽā¯", "you_dont_have_any_shared_links": "āŽ‰āŽ™ā¯āŽ•āŽŗāŽŋāŽŸāŽŽā¯ āŽĒāŽ•āŽŋāŽ°āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ‡āŽŖā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ āŽŽāŽ¤ā¯āŽĩā¯āŽŽā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ", "your_wifi_name": "āŽ‰āŽ™ā¯āŽ•āŽŗā¯ āŽĩā¯ˆāŽƒāŽĒ❈ āŽĒā¯†āŽ¯āŽ°ā¯", + "zero_to_clear_rating": "āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ āŽŽāŽ¤āŽŋāŽĒā¯āŽĒā¯€āŽŸā¯āŽŸā¯ˆ āŽ…āŽ´āŽŋāŽ•ā¯āŽ• 0 āŽ āŽ…āŽ´ā¯āŽ¤ā¯āŽ¤āŽĩā¯āŽŽā¯", "zoom_image": "āŽĒā¯†āŽ°āŽŋāŽ¤āŽžāŽ•ā¯āŽ• āŽĒāŽŸāŽŽā¯", "zoom_to_bounds": "āŽŽāŽ˛ā¯āŽ˛ā¯ˆāŽ•ā¯āŽ•ā¯ āŽĒā¯†āŽ°āŽŋāŽ¤āŽžāŽ•ā¯āŽ•ā¯" } diff --git a/i18n/te.json b/i18n/te.json index 18b38069b4..73288c1a8e 100644 --- a/i18n/te.json +++ b/i18n/te.json @@ -350,7 +350,7 @@ "user_settings": "ā°ĩā°žā°Ąāąā°•ā°°ā°ŋ ā°¸āą†ā°Ÿāąā°Ÿā°ŋā°‚ā°—āąâ€Œā°˛āą", "user_settings_description": "ā°ĩā°žā°Ąāąā°•ā°°ā°ŋ ā°¸āą†ā°Ÿāąā°Ÿā°ŋā°‚ā°—āąâ€Œā°˛ā°¨āą ā°¨ā°ŋā°°āąā°ĩā°šā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", "version_check_enabled_description": "ā°ĩā°°āąā°ˇā°¨āą ⰤⰍā°ŋā°–āą€ā°¨ā°ŋ ā°šāą‡ā°¯ā°‚ā°Ąā°ŋ", - "version_check_implications": "ā°ĩā°°āąā°ˇā°¨āą ⰤⰍā°ŋā°–āą€ ā°Ģāą€ā°šā°°āą github.comā°¤āą‹ ā°•āąā°°ā°Žā°žā°¨āąā°—ā°¤ ā°•ā°Žāąā°¯āą‚ā°¨ā°ŋā°•āą‡ā°ˇā°¨āąâ€Œā°Ēāąˆ ā°†ā°§ā°žā°°ā°Ēā°Ąāąā°¤āąā°‚ā°Ļā°ŋ", + "version_check_implications": "ā°ĩā°°āąā°ˇā°¨āą ⰤⰍā°ŋā°–āą€ ā°Ģāą€ā°šā°°āą {server}ā°¤āą‹ ā°•āąā°°ā°Žā°žā°¨āąā°—ā°¤ ā°•ā°Žāąā°¯āą‚ā°¨ā°ŋā°•āą‡ā°ˇā°¨āąâ€Œā°Ēāąˆ ā°†ā°§ā°žā°°ā°Ēā°Ąāąā°¤āąā°‚ā°Ļā°ŋ", "version_check_settings": "ā°ĩā°°āąā°ˇā°¨āą ⰤⰍā°ŋā°–āą€", "version_check_settings_description": "ā°•āąŠā°¤āąā°¤ ā°ĩā°°āąā°ˇā°¨āą ā°¨āą‹ā°Ÿā°ŋā°Ģā°ŋā°•āą‡ā°ˇā°¨āąâ€Œā°¨āą ā°Ēāąā°°ā°žā°°ā°‚ā°­ā°ŋā°‚ā°šā°‚ā°Ąā°ŋ/ā°†ā°Ēā°ŋā°ĩāą‡ā°¯ā°‚ā°Ąā°ŋ", "video_conversion_job": "ā°ĩāą€ā°Ąā°ŋā°¯āą‹ā°˛ā°¨āą ā°Ÿāąā°°ā°žā°¨āąā°¸āąâ€Œā°•āą‹ā°Ąāą ā°šāą‡ā°¯ā°‚ā°Ąā°ŋ", @@ -523,10 +523,6 @@ "date_range": "ā°¤āą‡ā°Ļāą€ ā°Ēā°°ā°ŋā°§ā°ŋ", "day": "ā°°āą‹ā°œāą", "deduplicate_all": "ā°…ā°¨āąā°¨āą€ ⰍⰕā°ŋā°˛āą€ā°˛āą ā°¤āąŠā°˛ā°—ā°ŋā°‚ā°šāą", - "deduplication_criteria_1": "ā°Ŧāąˆā°Ÿāąâ€Œā°˛ā°˛āą‹ Ⱊā°ŋā°¤āąā°° ā°Ēā°°ā°ŋā°Žā°žā°Ŗā°‚", - "deduplication_criteria_2": "EXIF ā°Ąāą‡ā°Ÿā°ž ā°¸ā°‚ā°–āąā°¯", - "deduplication_info": "ⰍⰕā°ŋā°˛āą€ā°˛ ā°¤āąŠā°˛ā°—ā°ŋā°‚ā°Ēāą ā°¸ā°Žā°žā°šā°žā°°ā°‚", - "deduplication_info_description": "ā°†ā°¸āąā°¤āąā°˛ā°¨āą ā°¸āąā°ĩā°¯ā°‚ā°šā°žā°˛ā°•ā°‚ā°—ā°ž ā°Žāąā°‚ā°Ļā°¸āąā°¤āąā°—ā°ž ā°Žā°‚ā°šāąā°•āą‹ā°ĩā°Ąā°žā°¨ā°ŋā°•ā°ŋ ā°Žā°°ā°ŋā°¯āą ⰍⰕā°ŋā°˛āą€ā°˛ā°¨āą ā°Ēāą†ā°Ļāąā°Ļā°ŽāąŠā°¤āąā°¤ā°‚ā°˛āą‹ ā°¤āąŠā°˛ā°—ā°ŋā°‚ā°šā°Ąā°žā°¨ā°ŋā°•ā°ŋ, ā°Žāą‡ā°Žāą ā°ĩāą€ā°Ÿā°ŋā°¨ā°ŋ ā°Ēā°°ā°ŋā°ļāą€ā°˛ā°ŋā°¸āąā°¤ā°žā°Žāą:", "delete": "ā°¤āąŠā°˛ā°—ā°ŋā°‚ā°šāą", "delete_album": "ā°†ā°˛āąā°Ŧā°Žāąâ€Œā°¨āą ā°¤āąŠā°˛ā°—ā°ŋā°‚ā°šāą", "delete_api_key_prompt": "ā°Žāą€ā°°āą Ⰸ API ā°•āą€ā°¨ā°ŋ ā°–ā°šāąā°šā°ŋā°¤ā°‚ā°—ā°ž ā°¤āąŠā°˛ā°—ā°ŋā°‚ā°šā°žā°˛ā°¨āąā°•āąā°‚ā°Ÿāąā°¨āąā°¨ā°žā°°ā°ž?", diff --git a/i18n/th.json b/i18n/th.json index f22c83dfb8..f0f70638fd 100644 --- a/i18n/th.json +++ b/i18n/th.json @@ -5,6 +5,7 @@ "acknowledge": "ā¸Ŗā¸ąā¸šā¸—ā¸Ŗā¸˛ā¸š", "action": "ā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗ", "action_common_update": "ā¸­ā¸ąā¸›āš€ā¸”ā¸•", + "action_description": "ā¸Šā¸¸ā¸”ā¸ā¸˛ā¸Ŗā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸—ā¸ĩāšˆā¸ˆā¸°ā¸›ā¸ā¸´ā¸šā¸ąā¸•ā¸´ā¸ā¸ąā¸šā¸Ŗā¸˛ā¸ĸ⏁⏞⏪⏗ā¸ĩāšˆā¸œāšˆā¸˛ā¸™ā¸ā¸˛ā¸Ŗā¸ā¸Ŗā¸­ā¸‡", "actions": "ā¸ā¸˛ā¸Ŗā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗ", "active": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸—ā¸ŗā¸‡ā¸˛ā¸™", "active_count": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸—ā¸ŗā¸‡ā¸˛ā¸™: {count}", @@ -16,11 +17,13 @@ "add_a_name": "āš€ā¸žā¸´āšˆā¸Ąā¸Šā¸ˇāšˆā¸­", "add_a_title": "āš€ā¸žā¸´āšˆā¸Ąā¸Ģā¸ąā¸§ā¸‚āš‰ā¸­", "add_action": "āš€ā¸žā¸´āšˆā¸Ąā¸ā¸˛ā¸Ŗā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗ", + "add_action_description": "⏄ā¸Ĩā¸´ā¸āš€ā¸žā¸ˇāšˆā¸­āš€ā¸žā¸´āšˆā¸Ąā¸ā¸˛ā¸Ŗā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗ", "add_assets": "āš€ā¸žā¸´āšˆā¸Ąā¸Ēā¸ˇāšˆā¸­", "add_birthday": "āš€ā¸žā¸´āšˆā¸Ąā¸§ā¸ąā¸™āš€ā¸ā¸´ā¸”", "add_endpoint": "āš€ā¸žā¸´āšˆā¸Ąā¸›ā¸Ĩ⏞ā¸ĸ⏗⏞⏇", "add_exclusion_pattern": "āš€ā¸žā¸´āšˆā¸Ąā¸‚āš‰ā¸­ā¸ĸā¸āš€ā¸§āš‰ā¸™", "add_filter": "āš€ā¸žā¸´āšˆā¸Ąā¸•ā¸ąā¸§ā¸ā¸Ŗā¸­ā¸‡", + "add_filter_description": "⏄ā¸Ĩā¸´ā¸āš€ā¸žā¸ˇāšˆā¸­āš€ā¸žā¸´āšˆā¸Ąā¸ā¸˛ā¸Ŗā¸ā¸Ŗā¸­ā¸‡", "add_location": "āš€ā¸žā¸´āšˆā¸Ąā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡", "add_more_users": "āš€ā¸žā¸´āšˆā¸Ąā¸œā¸šāš‰āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", "add_partner": "āš€ā¸žā¸´āšˆā¸Ąā¸„ā¸šāšˆā¸Ģā¸š", @@ -32,12 +35,14 @@ "add_to_album_bottom_sheet_added": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ {album} āšā¸Ĩāš‰ā¸§", "add_to_album_bottom_sheet_already_exists": "⏭ā¸ĸā¸šāšˆāšƒā¸™ {album} ⏭ā¸ĸā¸šāšˆāšā¸Ĩāš‰ā¸§", "add_to_album_bottom_sheet_some_local_assets": "āš„ā¸Ÿā¸ĨāšŒā¸šā¸˛ā¸‡ā¸Ēāšˆā¸§ā¸™āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāš„ā¸”āš‰", + "add_to_album_toggle": "ā¸Ēā¸Ĩā¸ąā¸šā¸ā¸˛ā¸Ŗāš€ā¸Ĩ⏎⏭⏁ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸š {album}", "add_to_albums": "āš€ā¸žā¸´āšˆā¸Ąāš€ā¸‚āš‰ā¸˛āšƒā¸™ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "add_to_albums_count": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą ({count})", "add_to_bottom_bar": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡", "add_to_shared_album": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ĩāšˆāšā¸Šā¸ŖāšŒ", "add_upload_to_stack": "āš€ā¸žā¸´āšˆā¸Ąā¸—ā¸ĩāšˆā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āš€ā¸‚āš‰ā¸˛ stack", "add_url": "āš€ā¸žā¸´āšˆā¸Ą URL", + "add_workflow_step": "āš€ā¸žā¸´āšˆā¸Ąā¸‚ā¸ąāš‰ā¸™ā¸•ā¸­ā¸™ā¸ā¸˛ā¸Ŗā¸—ā¸ŗā¸‡ā¸˛ā¸™", "added_to_archive": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ā¸—ā¸ĩāšˆā¸ˆā¸ąā¸”āš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗ", "added_to_favorites": "āš€ā¸žā¸´āšˆā¸Ąāš€ā¸‚āš‰ā¸˛ā¸Ŗā¸˛ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”āšā¸Ĩāš‰ā¸§", "added_to_favorites_count": "āš€ā¸žā¸´āšˆā¸Ą {count, number} ā¸Ŗā¸šā¸›āš€ā¸‚āš‰ā¸˛ā¸Ŗā¸˛ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”āšā¸Ĩāš‰ā¸§", @@ -70,6 +75,7 @@ "confirm_reprocess_all_faces": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸„ā¸¸ā¸“ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸›ā¸Ŗā¸°ā¸Ąā¸§ā¸Ĩ⏜ā¸Ĩāšƒā¸šā¸Ģā¸™āš‰ā¸˛ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”āšƒā¸Ģā¸Ąāšˆ? ā¸Šā¸ˇāšˆā¸­ā¸„ā¸™ā¸ˆā¸°ā¸–ā¸šā¸ā¸Ĩā¸šāš„ā¸›ā¸”āš‰ā¸§ā¸ĸ", "confirm_user_password_reset": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ŗā¸ĩāš€ā¸‹āš‡ā¸•ā¸Ŗā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™ā¸‚ā¸­ā¸‡ {user} ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?", "confirm_user_pin_code_reset": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ŗā¸ĩāš€ā¸‹āš‡ā¸•ā¸Ŗā¸Ģā¸ąā¸Ē PIN ⏂⏭⏇ {user}", + "copy_config_to_clipboard_description": "ā¸„ā¸ąā¸”ā¸Ĩā¸­ā¸ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸Ŗā¸°ā¸šā¸šā¸›ā¸ąā¸ˆā¸ˆā¸¸ā¸šā¸ąā¸™āšƒā¸™ā¸Ŗā¸šā¸›āšā¸šā¸š JSON āš„ā¸›ā¸ĸā¸ąā¸‡ā¸„ā¸Ĩā¸´ā¸›ā¸šā¸­ā¸ŖāšŒā¸”", "create_job": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸‡ā¸˛ā¸™", "cron_expression": "ā¸Ŗā¸šā¸›āšā¸šā¸š cron", "cron_expression_description": "ā¸•ā¸ąāš‰ā¸‡ā¸Šāšˆā¸§ā¸‡āš€ā¸§ā¸Ĩā¸˛āšƒā¸™ā¸ā¸˛ā¸Ŗā¸Ēāšā¸ā¸™āš‚ā¸”ā¸ĸāšƒā¸Šāš‰ā¸Ŗā¸šā¸›āšā¸šā¸š cron ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāš€ā¸žā¸´āšˆā¸Ąāš€ā¸•ā¸´ā¸Ąā¸ā¸Ŗā¸¸ā¸“ā¸˛ā¸­ā¸´ā¸‡ Crontab Guru", @@ -77,6 +83,7 @@ "disable_login": "⏛⏴⏔⏁⏞⏪ā¸Ĩāš‡ā¸­ā¸ā¸­ā¸´ā¸™", "duplicate_detection_job_description": "āšƒā¸Šāš‰ machine learning ā¸ā¸ąā¸šā¸Ēā¸ĩāšˆā¸­āš€ā¸žā¸ˇāšˆā¸­ā¸•ā¸Ŗā¸§ā¸ˆā¸ˆā¸ąā¸šā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸—ā¸ĩāšˆā¸„ā¸Ĩāš‰ā¸˛ā¸ĸā¸ā¸ąā¸™ āš‚ā¸”ā¸ĸāšƒā¸Šāš‰ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģā¸˛ā¸­ā¸ąā¸ˆā¸‰ā¸Ŗā¸´ā¸ĸ⏰", "exclusion_pattern_description": "ā¸‚āš‰ā¸­ā¸ĸā¸āš€ā¸§āš‰ā¸™ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩā¸°āš€ā¸§āš‰ā¸™āš„ā¸Ÿā¸ĨāšŒāšā¸Ĩā¸°āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸‚ā¸“ā¸°ā¸Ēāšā¸ā¸™ā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸žā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ ā¸Ąā¸ĩā¸›ā¸Ŗā¸°āš‚ā¸ĸā¸Šā¸™āšŒāš€ā¸Ąā¸ˇāšˆā¸­āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸Ąā¸ĩāš„ā¸Ÿā¸ĨāšŒā¸—ā¸ĩāšˆāš„ā¸Ąāšˆā¸­ā¸ĸā¸˛ā¸ā¸™ā¸ŗāš€ā¸‚āš‰ā¸˛ āš€ā¸Šāšˆā¸™āš„ā¸Ÿā¸ĨāšŒ RAW", + "export_config_as_json_description": "ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸Ŗā¸°ā¸šā¸šā¸›ā¸ąā¸ˆā¸ˆā¸¸ā¸šā¸ąā¸™āš„ā¸›ā¸ĸā¸ąā¸‡āš„ā¸Ÿā¸ĨāšŒāšƒā¸™ā¸Ŗā¸šā¸›āšā¸šā¸š JSON", "external_libraries_page_description": "ā¸Ģā¸™āš‰ā¸˛ā¸•āšˆā¸˛ā¸‡ā¸„ā¸Ĩā¸ąā¸‡āšā¸­ā¸”ā¸Ąā¸´ā¸™ā¸ ā¸˛ā¸ĸ⏙⏭⏁", "face_detection": "ā¸ā¸˛ā¸Ŗā¸•ā¸Ŗā¸§ā¸ˆā¸ˆā¸ąā¸šāšƒā¸šā¸Ģā¸™āš‰ā¸˛", "face_detection_description": "ā¸•ā¸Ŗā¸§ā¸ˆā¸ˆā¸ąā¸šāšƒā¸šā¸Ģā¸™āš‰ā¸˛āšƒā¸™ā¸Ēā¸ĩāšˆā¸­āš‚ā¸”ā¸ĸāšƒā¸Šāš‰ machine learning ⏧⏴⏔ā¸ĩāš‚ā¸­ā¸ˆā¸°āšƒā¸Šāš‰ā¸ ā¸˛ā¸žā¸•ā¸ąā¸§ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸ˆā¸˛ā¸ā¸§ā¸´ā¸”ā¸ĩāš‚ā¸­āš€ā¸—āšˆā¸˛ā¸™ā¸ąāš‰ā¸™ \"ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”\" ā¸ˆā¸°ā¸›ā¸Ŗā¸°ā¸Ąā¸§ā¸Ĩ⏜ā¸Ĩā¸Ēā¸ĩāšˆā¸­ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸” \"⏂⏞⏔ā¸Ģ⏞ā¸ĸ\" ā¸ˆā¸°ā¸›ā¸Ŗā¸°ā¸Ąā¸§ā¸Ĩ⏜ā¸Ĩā¸Ēā¸ĩāšˆā¸­ā¸—ā¸ĩāšˆā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆāš„ā¸”āš‰ā¸›ā¸Ŗā¸°ā¸Ąā¸§ā¸Ĩ⏜ā¸Ĩ āšƒā¸šā¸Ģā¸™āš‰ā¸˛ā¸—ā¸ĩāšˆā¸–ā¸šā¸ā¸•ā¸Ŗā¸§ā¸ˆā¸ˆā¸ąā¸šāšā¸Ĩāš‰ā¸§ā¸ˆā¸°ā¸–ā¸šā¸āš€ā¸‚āš‰ā¸˛ā¸„ā¸´ā¸§ā¸›ā¸Ŗā¸°ā¸Ąā¸§ā¸Ĩ⏜ā¸Ĩā¸ā¸˛ā¸Ŗā¸ˆā¸”ā¸ˆā¸ŗāšƒā¸šā¸Ģā¸™āš‰ā¸˛ āš€ā¸žā¸´āšˆā¸Ąāš€ā¸‚āš‰ā¸˛āš„ā¸›āšƒā¸™ā¸ā¸Ĩā¸¸āšˆā¸Ąā¸—ā¸ĩāšˆā¸Ąā¸ĩ⏭ā¸ĸā¸šāšˆāšā¸Ĩāš‰ā¸§ā¸Ģā¸Ŗā¸ˇā¸­ā¸„ā¸™āšƒā¸Ģā¸Ąāšˆ", @@ -97,6 +104,8 @@ "image_preview_description": "ā¸ ā¸˛ā¸žā¸‚ā¸™ā¸˛ā¸”ā¸›ā¸˛ā¸™ā¸ā¸Ĩ⏞⏇⏗ā¸ĩāšˆā¸–ā¸šā¸ā¸Ĩā¸šā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāš€ā¸Ąā¸•ā¸˛ āšƒā¸Šāš‰ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸ā¸˛ā¸Ŗā¸”ā¸šāšā¸­ā¸Ēāš€ā¸‹āš‡ā¸•āš€ā¸”ā¸ĩāšˆā¸ĸā¸§āšā¸Ĩ⏰ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸ā¸˛ā¸Ŗāš€ā¸Ŗā¸ĩā¸ĸā¸™ā¸Ŗā¸šāš‰ā¸‚ā¸­ā¸‡āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ (Machine Learning)", "image_preview_quality_description": "ā¸„ā¸¸ā¸“ā¸ ā¸˛ā¸žā¸ā¸˛ā¸Ŗāšā¸Ēā¸”ā¸‡ā¸•ā¸ąā¸§ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸•ā¸ąāš‰ā¸‡āšā¸•āšˆ 1-100 ā¸ĸā¸´āšˆā¸‡ā¸Ēā¸šā¸‡ā¸ĸā¸´āšˆā¸‡ā¸”ā¸ĩ āšā¸•āšˆā¸ˆā¸°ā¸—ā¸ŗāšƒā¸Ģāš‰āš„ā¸Ÿā¸ĨāšŒā¸Ąā¸ĩā¸‚ā¸™ā¸˛ā¸”āšƒā¸Ģā¸āšˆā¸‚ā¸ļāš‰ā¸™āšā¸Ĩā¸°ā¸­ā¸˛ā¸ˆā¸—ā¸ŗāšƒā¸Ģāš‰āšā¸­ā¸›ā¸•ā¸­ā¸šā¸Ēā¸™ā¸­ā¸‡ā¸Šāš‰ā¸˛ā¸Ĩ⏇ ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸•āšˆā¸ŗā¸­ā¸˛ā¸ˆā¸Ēāšˆā¸‡ā¸œā¸Ĩā¸•āšˆā¸­ā¸„ā¸¸ā¸“ā¸ ā¸˛ā¸ž Machine Learning", "image_preview_title": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸žā¸Ŗā¸ĩ⏧⏴⏧", + "image_progressive": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸šā¸šāš‚ā¸›ā¸Ŗāš€ā¸ā¸Ŗā¸Ē⏋ā¸ĩ⏟", + "image_progressive_description": "āš€ā¸‚āš‰ā¸˛ā¸Ŗā¸Ģā¸ąā¸Ēā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž JPEG āšā¸šā¸šāš‚ā¸›ā¸Ŗāš€ā¸ā¸Ŗā¸Ē⏋ā¸ĩā¸Ÿāš€ā¸žā¸ˇāšˆā¸­āšƒā¸Ģāš‰āšā¸Ēā¸”ā¸‡ā¸œā¸Ĩāšā¸šā¸šā¸„āšˆā¸­ā¸ĸāš† ā¸Šā¸ąā¸”ā¸‚ā¸ļāš‰ā¸™ā¸‚ā¸“ā¸°āš‚ā¸Ģā¸Ĩ⏔ ā¸—ā¸ąāš‰ā¸‡ā¸™ā¸ĩāš‰ā¸ˆā¸°āš„ā¸Ąāšˆā¸Ąā¸ĩ⏜ā¸Ĩā¸ā¸ąā¸šā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž WebP", "image_quality": "ā¸„ā¸¸ā¸“ā¸ ā¸˛ā¸ž", "image_resolution": "ā¸„ā¸§ā¸˛ā¸Ąā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸ⏔", "image_resolution_description": "ā¸„ā¸§ā¸˛ā¸Ąā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸ⏔ā¸Ēā¸šā¸ā¸§āšˆā¸˛ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸āš‡ā¸šā¸Ŗā¸˛ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸā¸”āš„ā¸”āš‰ā¸Ąā¸˛ā¸ā¸ā¸§āšˆā¸˛āšā¸•āšˆāšƒā¸Šāš‰āš€ā¸§ā¸Ĩ⏞ encode ā¸™ā¸˛ā¸™ā¸ā¸§āšˆā¸˛ āš„ā¸Ÿā¸ĨāšŒāšƒā¸Ģā¸āšˆā¸ā¸§āšˆā¸˛ āšā¸Ĩ⏰ā¸Ĩā¸”ā¸„ā¸§ā¸˛ā¸Ąā¸•ā¸­ā¸šā¸Ēā¸™ā¸­ā¸‡ā¸‚ā¸­ā¸‡āšā¸­ā¸›", @@ -105,6 +114,7 @@ "image_thumbnail_description": "ā¸Ŗā¸šā¸›ā¸‚ā¸™ā¸˛ā¸”ā¸ĸāšˆā¸­ā¸—ā¸ĩāšˆā¸Ąā¸ĩ⏁⏞⏪ā¸Ĩā¸šā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāš€ā¸Ąā¸•ā¸˛ā¸”āš‰ā¸˛ā¸•āš‰ā¸˛ āšƒā¸Šāš‰āš€ā¸Ąā¸ˇāšˆā¸­ā¸”ā¸šā¸ ā¸˛ā¸žā¸–āšˆā¸˛ā¸ĸāšƒā¸™ā¸ā¸Ĩā¸¸āšˆā¸Ą āš€ā¸Šāšˆā¸™ āšƒā¸™āš„ā¸—ā¸ĄāšŒāš„ā¸Ĩā¸™āšŒā¸Ģā¸Ĩā¸ąā¸", "image_thumbnail_quality_description": "ā¸„ā¸¸ā¸“ā¸ ā¸˛ā¸žā¸‚ā¸­ā¸‡ā¸ ā¸˛ā¸žā¸‚ā¸™ā¸˛ā¸”ā¸ĸāšˆā¸­ā¸•ā¸ąāš‰ā¸‡āšā¸•āšˆ 1-100 ā¸ĸā¸´āšˆā¸‡ā¸Ēā¸šā¸‡ā¸ĸā¸´āšˆā¸‡ā¸”ā¸ĩ āšā¸•āšˆā¸ˆā¸°ā¸—ā¸ŗāšƒā¸Ģāš‰āš„ā¸Ÿā¸ĨāšŒā¸Ąā¸ĩā¸‚ā¸™ā¸˛ā¸”āšƒā¸Ģā¸āšˆā¸‚ā¸ļāš‰ā¸™āšā¸Ĩā¸°ā¸­ā¸˛ā¸ˆā¸—ā¸ŗāšƒā¸Ģāš‰āšā¸­ā¸›ā¸•ā¸­ā¸šā¸Ēā¸™ā¸­ā¸‡ā¸Šāš‰ā¸˛ā¸Ĩ⏇", "image_thumbnail_title": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ Thumbnail", + "import_config_from_json_description": "ā¸™ā¸ŗāš€ā¸‚āš‰ā¸˛ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸Ŗā¸°ā¸šā¸šāš‚ā¸”ā¸ĸā¸ā¸˛ā¸Ŗā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āš„ā¸Ÿā¸ĨāšŒā¸„ā¸­ā¸™ā¸Ÿā¸´ā¸ JSON", "job_concurrency": "{job} ā¸‡ā¸˛ā¸™ā¸žā¸Ŗāš‰ā¸­ā¸Ąā¸ā¸ąā¸™", "job_created": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸‡ā¸˛ā¸™āš€ā¸Ŗā¸ĩā¸ĸā¸šā¸Ŗāš‰ā¸­ā¸ĸ", "job_not_concurrency_safe": "⏇⏞⏙⏙ā¸ĩāš‰ā¸—ā¸ŗā¸‡ā¸˛ā¸™ā¸žā¸Ŗāš‰ā¸­ā¸Ąā¸ā¸ąā¸™āšā¸šā¸šā¸›ā¸Ĩā¸­ā¸”ā¸ ā¸ąā¸ĸāš„ā¸Ąāšˆāš„ā¸”āš‰", @@ -422,7 +432,7 @@ "user_successfully_removed": "ā¸Ĩā¸šā¸œā¸šāš‰āšƒā¸Šāš‰ {email} āš€ā¸Ēā¸Ŗāš‡ā¸ˆā¸Ēā¸Ąā¸šā¸šā¸Ŗā¸“āšŒāšā¸Ĩāš‰ā¸§", "users_page_description": "ā¸Ģā¸™āš‰ā¸˛ā¸œā¸šāš‰āšƒā¸Šāš‰ā¸œā¸šāš‰ā¸”ā¸šāšā¸Ĩ", "version_check_enabled_description": "āš€ā¸Šāš‡ā¸„ GitHub āš€ā¸›āš‡ā¸™ā¸Ŗā¸°ā¸ĸ⏰ āš† āš€ā¸žā¸ˇāšˆā¸­ā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šā¸Ŗā¸¸āšˆā¸™āšƒā¸Ģā¸Ąāšˆ", - "version_check_implications": "ā¸ā¸˛ā¸Ŗā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šāš€ā¸§ā¸­ā¸ŖāšŒā¸Šā¸ąā¸™āšƒā¸Ģā¸Ąāšˆā¸ˆā¸°ā¸•āš‰ā¸­ā¸‡ā¸•ā¸´ā¸”ā¸•āšˆā¸­ā¸ā¸ąā¸š github.com āš€ā¸›āš‡ā¸™ā¸Ŗā¸°ā¸ĸ⏰", + "version_check_implications": "ā¸ā¸˛ā¸Ŗā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šāš€ā¸§ā¸­ā¸ŖāšŒā¸Šā¸ąā¸™āšƒā¸Ģā¸Ąāšˆā¸ˆā¸°ā¸•āš‰ā¸­ā¸‡ā¸•ā¸´ā¸”ā¸•āšˆā¸­ā¸ā¸ąā¸š {server} āš€ā¸›āš‡ā¸™ā¸Ŗā¸°ā¸ĸ⏰", "version_check_settings": "ā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šā¸Ŗā¸¸āšˆā¸™", "version_check_settings_description": "āš€ā¸›ā¸´ā¸”/ā¸›ā¸´ā¸”ā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™ā¸Ŗā¸¸āšˆā¸™āšƒā¸Ģā¸Ąāšˆ", "video_conversion_job": "āš€ā¸‚āš‰ā¸˛ā¸Ŗā¸Ģā¸ąā¸Ē⏧ā¸ĩ⏔ā¸ĩāš‚ā¸­ (transcode)", @@ -510,10 +520,10 @@ "always_keep_photos_hint": "\"āš€ā¸žā¸´āšˆā¸Ąā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆā¸§āšˆā¸˛ā¸‡\" ā¸ˆā¸°āš€ā¸āš‡ā¸šā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”ā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸™ā¸ĩāš‰", "always_keep_videos_hint": "\"āš€ā¸žā¸´āšˆā¸Ąā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆā¸§āšˆā¸˛ā¸‡\" ā¸ˆā¸°āš€ā¸āš‡ā¸šā¸§ā¸´ā¸”ā¸ĩāš‚ā¸­ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”ā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸™ā¸ĩāš‰", "anti_clockwise": "ā¸—ā¸§ā¸™āš€ā¸‚āš‡ā¸Ąā¸™ā¸˛ā¸Ŧ⏴⏁⏞", - "api_key": "API key", + "api_key": "⏄ā¸ĩā¸ĸāšŒ API", "api_key_description": "ā¸„āšˆā¸˛ā¸™ā¸ĩāš‰ā¸ˆā¸°āšā¸Ēā¸”ā¸‡āš€ā¸žā¸ĩā¸ĸā¸‡ā¸„ā¸Ŗā¸ąāš‰ā¸‡āš€ā¸”ā¸ĩā¸ĸ⏧ āš‚ā¸›ā¸Ŗā¸”ā¸„ā¸ąā¸”ā¸Ĩā¸­ā¸ā¸āšˆā¸­ā¸™ā¸›ā¸´ā¸”ā¸Ģā¸™āš‰ā¸˛ā¸•āšˆā¸˛ā¸‡", - "api_key_empty": "ā¸Šā¸ˇāšˆā¸­ API Key ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš„ā¸Ąāšˆā¸„ā¸§ā¸Ŗā¸§āšˆā¸˛ā¸‡āš€ā¸›ā¸Ĩāšˆā¸˛", - "api_keys": "API Key", + "api_key_empty": "ā¸Šā¸ˇāšˆā¸­ā¸„ā¸ĩā¸ĸāšŒ API ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš„ā¸Ąāšˆā¸„ā¸§ā¸Ŗā¸§āšˆā¸˛ā¸‡āš€ā¸›ā¸Ĩāšˆā¸˛", + "api_keys": "⏄ā¸ĩā¸ĸāšŒ API", "app_architecture_variant": "ā¸Ŗā¸šā¸›āšā¸šā¸š (ā¸Ēā¸–ā¸˛ā¸›ā¸ąā¸•ā¸ĸā¸ā¸Ŗā¸Ŗā¸Ą)", "app_bar_signout_dialog_content": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸­ā¸ĸ⏞⏁⏭⏭⏁⏈⏞⏁⏪⏰⏚⏚", "app_bar_signout_dialog_ok": "āšƒā¸Šāšˆ", @@ -864,14 +874,10 @@ "day": "ā¸§ā¸ąā¸™", "days": "ā¸§ā¸ąā¸™", "deduplicate_all": "ā¸Ŗā¸§ā¸Ąāš€ā¸‚āš‰ā¸˛ā¸”āš‰ā¸§ā¸ĸā¸ā¸ąā¸™ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", - "deduplication_criteria_1": "ā¸‚ā¸™ā¸˛ā¸”āš„ā¸šā¸•āšŒā¸‚ā¸­ā¸‡ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", - "deduplication_criteria_2": "ā¸ˆā¸ŗā¸™ā¸§ā¸™ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ EXIF", - "deduplication_info": "ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸ā¸˛ā¸Ŗā¸‚ā¸ˆā¸ąā¸”ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸‹āš‰ā¸ŗā¸‹āš‰ā¸­ā¸™", - "deduplication_info_description": "āš€ā¸Ĩ⏎⏭⏁ā¸Ēā¸ˇāšˆā¸­ā¸Ĩāšˆā¸§ā¸‡ā¸Ģā¸™āš‰ā¸˛āš‚ā¸”ā¸ĸā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´āšā¸Ĩ⏰ā¸Ĩ⏚⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗā¸‹āš‰ā¸ŗā¸‹āš‰ā¸­ā¸™ā¸ˆā¸ŗā¸™ā¸§ā¸™ā¸Ąā¸˛ā¸ āš€ā¸Ŗā¸˛ā¸ˆā¸°ā¸”ā¸šā¸—ā¸ĩāšˆ:", "delete": "ā¸Ĩ⏚⏭⏭⏁", "delete_action_prompt": "ā¸Ĩ⏚ {count} ⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗāšā¸Ĩāš‰ā¸§", "delete_album": "ā¸Ĩā¸šā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", - "delete_api_key_prompt": "ā¸„ā¸¸ā¸“ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ĩ⏚ API ⏄ā¸ĩā¸ĸāšŒ ⏙ā¸ĩāš‰āšƒā¸Šāšˆāš„ā¸Ģā¸Ą ?", + "delete_api_key_prompt": "ā¸„ā¸¸ā¸“ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ĩā¸šā¸„ā¸ĩā¸ĸāšŒ API ⏙ā¸ĩāš‰ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?", "delete_dialog_alert": "⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗā¸”ā¸ąā¸‡ā¸ā¸Ĩāšˆā¸˛ā¸§ā¸ˆā¸°ā¸–ā¸šā¸ā¸Ĩ⏚⏈⏞⏁ Immich āšā¸Ĩā¸°āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸–ā¸˛ā¸§ā¸Ŗ", "delete_dialog_alert_local": "⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗā¸”ā¸ąā¸‡ā¸ā¸Ĩāšˆā¸˛ā¸§ā¸ˆā¸°ā¸–ā¸šā¸ā¸Ĩā¸šā¸ˆā¸˛ā¸āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ā¸„ā¸¸ā¸“ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸–ā¸˛ā¸§ā¸Ŗ āšā¸•āšˆā¸ˆā¸°ā¸ĸā¸ąā¸‡ā¸„ā¸‡ā¸­ā¸ĸā¸šāšˆā¸šā¸™āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ Immich", "delete_dialog_alert_local_non_backed_up": "⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗā¸šā¸˛ā¸‡ā¸•ā¸ąā¸§āš„ā¸Ąāšˆāš„ā¸”āš‰ā¸–ā¸šā¸ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸šā¸™ Immich āšā¸Ĩā¸°ā¸ˆā¸°ā¸–ā¸šā¸ā¸Ĩā¸šā¸ˆā¸˛ā¸āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ā¸„ā¸¸ā¸“ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸–ā¸˛ā¸§ā¸Ŗ", @@ -1066,7 +1072,7 @@ "unable_to_connect": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­āš„ā¸”āš‰", "unable_to_copy_to_clipboard": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸„ā¸ąā¸”ā¸Ĩā¸­ā¸āš„ā¸›ā¸ĸā¸ąā¸‡ā¸„ā¸Ĩā¸´ā¸›ā¸šā¸­ā¸ŖāšŒā¸”āš„ā¸”āš‰ ā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šāšƒā¸Ģāš‰āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸„ā¸¸ā¸“āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļ⏇ā¸Ģā¸™āš‰ā¸˛ā¸œāšˆā¸˛ā¸™ā¸—ā¸˛ā¸‡ https", "unable_to_create_admin_account": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸šā¸ąā¸ā¸Šā¸ĩā¸œā¸šāš‰ā¸”ā¸šāšā¸Ĩā¸Ŗā¸°ā¸šā¸šāš„ā¸”āš‰", - "unable_to_create_api_key": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ API ⏄ā¸ĩā¸ĸāšŒ āš„ā¸”āš‰", + "unable_to_create_api_key": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸„ā¸ĩā¸ĸāšŒ API", "unable_to_create_library": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸žāš„ā¸”āš‰", "unable_to_create_user": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸œā¸šāš‰āšƒā¸Šāš‰āš„ā¸”āš‰", "unable_to_delete_album": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩā¸šā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāš„ā¸”āš‰", @@ -1093,7 +1099,7 @@ "unable_to_reassign_assets_new_person": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ąā¸­ā¸šā¸Ģā¸Ąā¸˛ā¸ĸ āšƒā¸Ģāš‰ā¸ā¸ąā¸šā¸šā¸¸ā¸„ā¸„ā¸Ĩāšƒā¸Ģā¸Ąāšˆāš„ā¸”āš‰", "unable_to_refresh_user": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ŗā¸ĩāš€ā¸Ÿā¸Ŗā¸Šā¸œā¸šāš‰āšƒā¸Šāš‰āš„ā¸”āš‰", "unable_to_remove_album_users": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩā¸šā¸œā¸šāš‰āšƒā¸Šāš‰ā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāš„ā¸”āš‰", - "unable_to_remove_api_key": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩ⏚ API Key āš„ā¸”āš‰", + "unable_to_remove_api_key": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩā¸šā¸„ā¸ĩā¸ĸāšŒ API", "unable_to_remove_assets_from_shared_link": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩ⏚⏭⏭⏁⏈⏞⏁ā¸Ĩā¸´ā¸‡ā¸āšŒā¸—ā¸ĩāšˆāšā¸Šā¸ŖāšŒāš„ā¸”āš‰", "unable_to_remove_library": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩā¸šā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸žāš„ā¸”āš‰", "unable_to_remove_partner": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩā¸šā¸„ā¸šāšˆā¸Ģā¸šāš„ā¸”āš‰", @@ -1105,7 +1111,7 @@ "unable_to_restore_trash": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸Ŗā¸ĩā¸ĸā¸ā¸„ā¸ˇā¸™ā¸–ā¸ąā¸‡ā¸‚ā¸ĸā¸°āš„ā¸”āš‰", "unable_to_restore_user": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸Ŗā¸ĩā¸ĸā¸ā¸„ā¸ˇā¸™ā¸œā¸šāš‰āšƒā¸Šāš‰āš„ā¸”āš‰", "unable_to_save_album": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸šā¸ąā¸™ā¸—ā¸ļā¸ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāš„ā¸”āš‰", - "unable_to_save_api_key": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸šā¸ąā¸™ā¸—ā¸ļ⏁ API ⏄ā¸ĩā¸ĸāšŒ āš„ā¸”āš‰", + "unable_to_save_api_key": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸šā¸ąā¸™ā¸—ā¸ļ⏁⏄ā¸ĩā¸ĸāšŒ API", "unable_to_save_date_of_birth": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸šā¸ąā¸™ā¸—ā¸ļā¸ā¸§ā¸ąā¸™āš€ā¸ā¸´ā¸”āš„ā¸”āš‰", "unable_to_save_name": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸šā¸ąā¸™ā¸—ā¸ļā¸ā¸Šā¸ˇāšˆā¸­āš„ā¸”āš‰", "unable_to_save_profile": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸šā¸ąā¸™ā¸—ā¸ļā¸āš‚ā¸›ā¸Ŗāš„ā¸Ÿā¸ĨāšŒāš„ā¸”āš‰", @@ -1199,7 +1205,7 @@ "geolocation_instruction_location": "⏄ā¸Ĩā¸´ā¸ā¸šā¸™ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸Ąā¸ĩā¸žā¸´ā¸ā¸ąā¸” GPS āš€ā¸žā¸ˇāšˆā¸­āšƒā¸Šāš‰ā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡ā¸™ā¸ąāš‰ā¸™ ā¸Ģā¸Ŗā¸ˇā¸­āš€ā¸Ĩā¸ˇā¸­ā¸ā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡ā¸ˆā¸˛ā¸āšā¸œā¸™ā¸—ā¸ĩāšˆāš‚ā¸”ā¸ĸ⏕⏪⏇", "get_help": "ā¸‚ā¸­ā¸„ā¸§ā¸˛ā¸Ąā¸Šāšˆā¸§ā¸ĸāš€ā¸Ģā¸Ĩ⏎⏭", "get_people_error": "ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔⏂⏓⏰⏔ā¸ļā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸œā¸šāš‰ā¸„ā¸™", - "get_wifiname_error": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ŗā¸ąā¸šā¸Šā¸ˇāšˆā¸­ Wi-Fi ⏁⏪⏏⏓⏞ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸ā¸˛ā¸Ŗāšƒā¸Ģāš‰ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•āšā¸­ā¸ž āšā¸Ĩ⏰ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸§āšˆā¸˛ Wi-Fi āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ā¸­ā¸ĸā¸šāšˆ", + "get_wifiname_error": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ŗā¸ąā¸šā¸Šā¸ˇāšˆā¸­ Wi-Fi ⏁⏪⏏⏓⏞ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸ā¸˛ā¸Ŗāšƒā¸Ģāš‰ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•āšā¸­ā¸› āšā¸Ĩ⏰ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸§āšˆā¸˛āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ā¸ā¸ąā¸šāš€ā¸„ā¸Ŗā¸ˇā¸­ā¸‚āšˆā¸˛ā¸ĸ Wi-Fi ⏭ā¸ĸā¸šāšˆ", "getting_started": "āš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", "go_back": "⏁ā¸Ĩā¸ąā¸š", "go_to_folder": "āš„ā¸›ā¸—ā¸ĩāšˆāš‚ā¸Ÿā¸ĨāšŒāš€ā¸”ā¸­ā¸ŖāšŒ", @@ -1434,7 +1440,7 @@ "manage_sharing_with_partners": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗāšā¸Šā¸ŖāšŒā¸ā¸ąā¸šā¸„ā¸šāšˆā¸Ģā¸š", "manage_the_app_settings": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛āšā¸­ā¸›", "manage_your_account": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸šā¸ąā¸ā¸Šā¸ĩ⏂⏭⏇⏄⏏⏓", - "manage_your_api_keys": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸¸ā¸āšā¸ˆ API ⏂⏭⏇⏄⏏⏓", + "manage_your_api_keys": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸„ā¸ĩā¸ĸāšŒ API ⏂⏭⏇⏄⏏⏓", "manage_your_devices": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“", "manage_your_oauth_connection": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗāš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ OAuth ⏂⏭⏇⏄⏏⏓", "map": "āšā¸œā¸™ā¸—ā¸ĩāšˆ", @@ -1520,7 +1526,7 @@ "networking_subtitle": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸›ā¸Ĩ⏞ā¸ĸā¸—ā¸˛ā¸‡āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", "never": "āš„ā¸Ąāšˆāš€ā¸„ā¸ĸ", "new_album": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšƒā¸Ģā¸Ąāšˆ", - "new_api_key": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ API ⏄ā¸ĩā¸ĸāšŒāšƒā¸Ģā¸Ąāšˆ", + "new_api_key": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸„ā¸ĩā¸ĸāšŒ API āšƒā¸Ģā¸Ąāšˆ", "new_date_range": "ā¸Šāšˆā¸§ā¸‡ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆāšƒā¸Ģā¸Ąāšˆ", "new_password": "⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™āšƒā¸Ģā¸Ąāšˆ", "new_person": "ā¸„ā¸™āšƒā¸Ģā¸Ąāšˆ", @@ -1585,7 +1591,7 @@ "on_this_device": "ā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸™ā¸ĩāš‰", "onboarding": "ā¸ā¸˛ā¸Ŗāš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", "onboarding_locale_description": "āš€ā¸Ĩā¸ˇā¸­ā¸ā¸ ā¸˛ā¸Šā¸˛ā¸—ā¸ĩāšˆā¸„ā¸¸ā¸“ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗ ⏄⏏⏓ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āš„ā¸”āš‰ā¸ ā¸˛ā¸ĸā¸Ģā¸Ĩā¸ąā¸‡āšƒā¸™ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛", - "onboarding_privacy_description": "⏟ā¸ĩāš€ā¸ˆā¸­ā¸ŖāšŒ (ā¸•ā¸ąā¸§āš€ā¸Ĩ⏎⏭⏁) ā¸•āšˆā¸­āš„ā¸›ā¸™ā¸ĩāš‰ā¸•āš‰ā¸­ā¸‡ā¸­ā¸˛ā¸¨ā¸ąā¸ĸ⏚⏪⏴⏁⏞⏪⏠⏞ā¸ĸ⏙⏭⏁ āšā¸Ĩ⏰ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™āš„ā¸”āš‰ā¸•ā¸Ĩā¸­ā¸”āš€ā¸§ā¸Ĩā¸˛āšƒā¸™ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗ", + "onboarding_privacy_description": "⏄⏏⏓ā¸Ēā¸Ąā¸šā¸ąā¸•ā¸´ (ā¸•ā¸ąā¸§āš€ā¸Ĩ⏎⏭⏁) ā¸•āšˆā¸­āš„ā¸›ā¸™ā¸ĩāš‰ā¸•āš‰ā¸­ā¸‡ā¸­ā¸˛ā¸¨ā¸ąā¸ĸ⏚⏪⏴⏁⏞⏪⏠⏞ā¸ĸ⏙⏭⏁ āšā¸Ĩ⏰ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™āš„ā¸”āš‰ā¸•ā¸Ĩā¸­ā¸”āš€ā¸§ā¸Ĩā¸˛āšƒā¸™ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛", "onboarding_server_welcome_description": "ā¸Ąā¸˛ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ā¸”āš‰ā¸§ā¸ĸā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸—ā¸ĩāšˆāšƒā¸Šāš‰ā¸šāšˆā¸­ā¸ĸā¸ā¸ąā¸™āš€ā¸–ā¸­ā¸°", "onboarding_theme_description": "āš€ā¸Ĩ⏎⏭⏁⏘ā¸ĩā¸Ąā¸Ēā¸ĩ ⏄⏏⏓ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āšā¸›ā¸Ĩā¸‡āš„ā¸”āš‰āšƒā¸™ā¸ ā¸˛ā¸ĸā¸Ģā¸Ĩā¸ąā¸‡āšƒā¸™ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“", "onboarding_user_welcome_description": "ā¸Ąā¸˛āš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™ā¸ā¸ąā¸™āš€ā¸–ā¸­ā¸°!", @@ -1635,7 +1641,7 @@ "pattern": "ā¸Ŗā¸šā¸›āšā¸šā¸š", "pause": "ā¸Ģā¸ĸ⏏⏔", "pause_memories": "ā¸Ģā¸ĸā¸¸ā¸”ā¸”ā¸šā¸„ā¸§ā¸˛ā¸Ąā¸—ā¸Ŗā¸‡ā¸ˆāšā¸˛", - "paused": "ā¸Ģā¸ĸ⏏⏔", + "paused": "ā¸Ģā¸ĸā¸¸ā¸”ā¸Šā¸ąāšˆā¸§ā¸„ā¸Ŗā¸˛ā¸§", "pending": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸Ŗā¸­", "people": "ā¸œā¸šāš‰ā¸„ā¸™", "people_edits_count": "{count, plural, one {# person} other {# people}} ā¸–ā¸šā¸āšā¸āš‰āš„ā¸‚", @@ -1648,11 +1654,13 @@ "permanently_delete_assets_prompt": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ĩ⏚ {count, plural, one {this asset?} other {these # asset?}}⏭ā¸ĸāšˆā¸˛ā¸‡ā¸–ā¸˛ā¸§ā¸Ŗ ā¸ā¸˛ā¸Ŗā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸™ā¸ĩāš‰ā¸ˆā¸°ā¸Ĩ⏚ {count, plural, one {it from its} other {them from their}} ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸”āš‰ā¸§ā¸ĸ", "permanently_deleted_asset": "ā¸Ĩ⏚ā¸Ēā¸ˇāšˆā¸­ā¸–ā¸˛ā¸§ā¸Ŗāšā¸Ĩāš‰ā¸§", "permanently_deleted_assets_count": "ā¸Ĩ⏚ {count, plural, one {# asset} other {# assets}} āš€ā¸Ŗā¸ĩā¸ĸā¸šā¸Ŗāš‰ā¸­ā¸ĸāšā¸Ĩāš‰ā¸§", + "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_denied": "ā¸Ēā¸´ā¸—ā¸˜ā¸´āšŒā¸–ā¸šā¸ā¸›ā¸ā¸´āš€ā¸Ē⏘ ā¸ā¸Ŗā¸¸ā¸“ā¸˛āšƒā¸Ģāš‰ā¸Ēā¸´ā¸—ā¸˜ā¸´āšŒāš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­āš€ā¸žā¸ˇāšˆā¸­āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™ Immich", "permission_onboarding_permission_granted": "āšƒā¸Ģāš‰ā¸Ēā¸´ā¸—ā¸˜ā¸´āšŒā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ ā¸„ā¸¸ā¸“ā¸žā¸Ŗāš‰ā¸­ā¸Ąāšƒā¸Šāš‰ā¸‡ā¸˛ā¸™āšā¸Ĩāš‰ā¸§", "permission_onboarding_permission_limited": "ā¸Ēā¸´ā¸—ā¸˜āšŒā¸ˆā¸ŗā¸ā¸ąā¸” āš€ā¸žā¸ˇāšˆā¸­āšƒā¸Ģāš‰ Immich ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšā¸Ĩā¸°ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸žāš„ā¸”āš‰ ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸Ēā¸´ā¸—ā¸˜ā¸´āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­", "permission_onboarding_request": "Immich ā¸ˆā¸ŗāš€ā¸›āš‡ā¸™ā¸ˆā¸°ā¸•āš‰ā¸­ā¸‡āš„ā¸”āš‰ā¸Ŗā¸ąā¸šā¸Ēā¸´ā¸—ā¸˜ā¸´āšŒā¸”ā¸šā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­", @@ -1787,7 +1795,7 @@ "remove_photo_from_memory": "ā¸Ĩā¸šā¸Ŗā¸šā¸›ā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸„ā¸§ā¸˛ā¸Ąā¸—ā¸Ŗā¸‡ā¸ˆā¸ŗā¸™ā¸ĩāš‰", "remove_url": "ā¸Ĩ⏚ URL", "remove_user": "ā¸Ĩā¸šā¸œā¸šāš‰āšƒā¸Šāš‰", - "removed_api_key": "API ⏄ā¸ĩā¸ĸāšŒā¸‚ā¸­ā¸‡: {name} ā¸–ā¸šā¸ā¸Ĩā¸šāšā¸Ĩāš‰ā¸§", + "removed_api_key": "ā¸Ĩā¸šā¸„ā¸ĩā¸ĸāšŒ API āšā¸Ĩāš‰ā¸§: {name}", "removed_from_archive": "ā¸Ĩā¸šā¸ˆā¸˛ā¸āš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗāšā¸Ĩāš‰ā¸§", "removed_from_favorites": "ā¸Ĩ⏚⏈⏞⏁⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”āšā¸Ĩāš‰ā¸§", "removed_from_favorites_count": "{count, plural, other {ā¸–ā¸šā¸ā¸Ĩ⏚#}} ⏈⏞⏁⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”āšā¸Ĩāš‰ā¸§", @@ -1835,7 +1843,7 @@ "save": "ā¸šā¸ąā¸™ā¸—ā¸ļ⏁", "save_to_gallery": "ā¸šā¸ąā¸™ā¸—ā¸ļā¸āš„ā¸›ā¸ĸā¸ąā¸‡āšā¸ā¸Ĩāš€ā¸Ĩ⏭⏪ā¸ĩ", "saved": "ā¸šā¸ąā¸™ā¸—ā¸ļā¸āšā¸Ĩāš‰ā¸§", - "saved_api_key": "ā¸šā¸ąā¸™ā¸—ā¸ļ⏁ API ⏄ā¸ĩā¸ĸāšŒ āšā¸Ĩāš‰ā¸§", + "saved_api_key": "ā¸šā¸ąā¸™ā¸—ā¸ļ⏁⏄ā¸ĩā¸ĸāšŒ API āšā¸Ĩāš‰ā¸§", "saved_profile": "āšā¸āš‰āš„ā¸‚āš‚ā¸›ā¸Ŗāš„ā¸Ÿā¸ĨāšŒā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "saved_settings": "ā¸šā¸ąā¸™ā¸—ā¸ļā¸ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "say_something": "ā¸žā¸šā¸”ā¸­ā¸°āš„ā¸Ŗā¸Ēā¸ąā¸ā¸­ā¸ĸāšˆā¸˛ā¸‡", @@ -2127,9 +2135,12 @@ "sync_upload_album_setting_subtitle": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āšā¸Ĩā¸°ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ĩāšˆāš€ā¸Ĩā¸ˇā¸­ā¸ā¸šā¸™ Immich", "tag": "āšā¸—āš‡ā¸", "tag_created": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āšā¸—āš‡ā¸: {tag}", + "tag_face": "āšā¸—āš‡ā¸āšƒā¸šā¸Ģā¸™āš‰ā¸˛", + "tag_feature_description": "ā¸”ā¸šā¸Ŗā¸šā¸›ā¸–āšˆā¸˛ā¸ĸāšā¸Ĩ⏰⏧ā¸ĩ⏔ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸ā¸Ĩā¸¸āšˆā¸Ąā¸•ā¸˛ā¸Ąā¸Ģā¸ąā¸§ā¸‚āš‰ā¸­āšā¸—āš‡ā¸", "tag_not_found_question": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ģā¸˛āšā¸—āš‡ā¸āš„ā¸”āš‰āšƒā¸Šāšˆā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āšā¸—āš‡ā¸āšƒā¸Ģā¸Ąāšˆ", "tag_people": "āšā¸—āš‡ā¸ā¸œā¸šāš‰ā¸„ā¸™", "tag_updated": "āšā¸—āš‡ā¸ā¸—ā¸ĩāšˆā¸–ā¸šā¸ā¸­ā¸ąā¸žāš€ā¸”ā¸•: {tag}", + "tagged_assets": "⏗ā¸ĩāšˆā¸–ā¸šā¸āšā¸—āš‡ā¸", "tags": "āšā¸—āš‡ā¸", "tap_to_run_job": "āšā¸•ā¸°āš€ā¸žā¸ˇāšˆā¸­ā¸Ŗā¸ąā¸™ā¸‡ā¸˛ā¸™", "template": "āš€ā¸—āš‡ā¸Ąāš€ā¸žā¸Ĩ⏕", @@ -2227,7 +2238,7 @@ "upload_status_duplicates": "⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗā¸‹āš‰ā¸ŗ", "upload_status_errors": "ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔", "upload_status_uploaded": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āšā¸Ĩāš‰ā¸§", - "upload_success": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ ⏪ā¸ĩāš€ā¸Ÿā¸Ŗā¸Šā¸Ģā¸™āš‰ā¸˛āš€ā¸žā¸ˇāšˆā¸­ā¸”ā¸šā¸Ēā¸ˇāšˆā¸­āšƒā¸Ģā¸Ąāšˆā¸—ā¸ĩāšˆā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔ā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", + "upload_success": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ ⏪ā¸ĩāš€ā¸Ÿā¸Ŗā¸Šā¸Ģā¸™āš‰ā¸˛āš€ā¸žā¸ˇāšˆā¸­ā¸”ā¸šā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āšƒā¸Ģā¸Ąāšˆ", "upload_to_immich": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āš„ā¸›ā¸ĸā¸ąā¸‡ Immich ({count})", "uploading": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔", "uploading_media": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ˇāšˆā¸­", diff --git a/i18n/tr.json b/i18n/tr.json index 66813d7d9d..e728c73a22 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -208,12 +208,12 @@ "manage_concurrency": "AynÄą anda çalÄąÅŸmayÄą yÃļnet", "manage_concurrency_description": "İş eşzamanlÄąlığınÄą yÃļnetmek için işler sayfasÄąna gidin", "manage_log_settings": "GÃŧnlÃŧk ayarlarÄąnÄą yÃļnet", - "map_dark_style": "Koyu mod", + "map_dark_style": "Koyu stil", "map_enable_description": "Harita ayarlarÄąnÄą etkinleştir", "map_gps_settings": "Harita & GPS AyarlarÄą", "map_gps_settings_description": "Harita YÃļnetimi & GPS (Ters Jeokodlama) AyarlarÄą", "map_implications": "Harita Ãļzelliği, harici bir dÃļşeme hizmetine (tiles.immich.cloud) bağlÄądÄąr", - "map_light_style": "AÃ§Äąk mod", + "map_light_style": "AÃ§Äąk stil", "map_manage_reverse_geocoding_settings": "Coğrafi Kodlama ayarlarÄąnÄą yÃļnet", "map_reverse_geocoding": "Coğrafi Kodlama", "map_reverse_geocoding_enable_description": "Coğrafi KodlamayÄą etkinleştir", @@ -441,7 +441,7 @@ "user_successfully_removed": "KullanÄącÄą {email} başarÄąyla kaldÄąrÄąldÄą.", "users_page_description": "YÃļnetici kullanÄącÄąlar sayfasÄą", "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_implications": "SÃŧrÃŧm kontrol Ãļzelliği, {server} ile periyodik iletişime dayanÄąr", "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", @@ -849,9 +849,12 @@ "create_link_to_share": "Paylaşmak için link oluştur", "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_face": "Yeni yÃŧz oluştur", "create_new_person": "Yeni kişi oluştur", "create_new_person_hint": "Seçili Ãļğeleri yeni bir kişiye atayÄąn", "create_new_user": "Yeni kullanÄącÄą oluştur", + "create_person": "Kişi oluştur", + "create_person_subtitle": "Seçilen yÃŧze bir isim ekleyerek yeni kişiyi oluşturun ve etiketleyin", "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", @@ -866,6 +869,7 @@ "crop_aspect_ratio_fixed": "Sabitlenmiş", "crop_aspect_ratio_free": "Boş", "crop_aspect_ratio_original": "Orijinal", + "crop_aspect_ratio_square": "Kare", "curated_object_page_title": "Nesneler", "current_device": "Mevcut cihaz", "current_pin_code": "Mevcut PIN kodu", @@ -880,7 +884,7 @@ "daily_title_text_date": "dd MMM E", "daily_title_text_date_year": "dd MMM yyyy E", "dark": "Koyu", - "dark_theme": "KaranlÄąk temaya geç", + "dark_theme": "Koyu temaya geç", "date": "Tarih", "date_after": "Sonraki tarih", "date_and_time": "Tarih ve Zaman", @@ -891,10 +895,8 @@ "day": "GÃŧn", "days": "GÃŧnler", "deduplicate_all": "TÃŧm kopyalarÄą kaldÄąr", - "deduplication_criteria_1": "Resim boyutu (bayt olarak)", - "deduplication_criteria_2": "EXIF veri sayÄąsÄą", - "deduplication_info": "Tekilleştirme Bilgileri", - "deduplication_info_description": "Öğeleri otomatik olarak Ãļnceden seçmek ve yinelenenleri toplu olarak kaldÄąrmak için şunlara bakÄąyoruz:", + "default_locale": "VarsayÄąlan Dil", + "default_locale_description": "Tarih ve sayÄąlarÄą tarayÄącÄąnÄązÄąn yerel ayarlarÄąna gÃļre biçimlendirin", "delete": "Sil", "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", @@ -970,7 +972,7 @@ "downloading_media": "Medya indiriliyor", "drop_files_to_upload": "DosyalarÄą yÃŧklemek için herhangi bir yere bÄąrakÄąn", "duplicates": "Kopyalar", - "duplicates_description": "Her grubu çÃļzmek için, varsa hangilerinin kopya olduğunu belirtin", + "duplicates_description": "Her bir grubu, varsa tekrarlanan Ãļğeleri belirterek çÃļzÃŧmleyin.", "duration": "SÃŧre", "edit": "DÃŧzenle", "edit_album": "AlbÃŧmÃŧ dÃŧzenle", @@ -1256,7 +1258,7 @@ "group_year": "YÄąla gÃļre grupla", "haptic_feedback_switch": "Dokunsal geri bildirimi aç", "haptic_feedback_title": "Dokunsal Geri Bildirim (Haptic Feedback)", - "has_quota": "Kota var", + "has_quota": "KotasÄą var", "hash_asset": "Karma Ãļğe", "hashed_assets": "Karma Ãļğeler", "hashing": "Hashleme", @@ -1387,9 +1389,11 @@ "library_page_sort_title": "AlbÃŧm başlığı", "licenses": "Lisanslar", "light": "AÃ§Äąk", + "light_theme": "AÃ§Äąk temaya geç", "like": "Beğen", "like_deleted": "Beğeni silindi", "link_motion_video": "Hareket videosunu bağla", + "link_to_docs": "Daha fazla bilgi için belgelere bakÄąn.", "link_to_oauth": "OAuth'a bağla", "linked_oauth_account": "BağlÄą OAuth hesabÄą", "list": "Liste", @@ -1498,7 +1502,7 @@ "map_no_location_permission_content": "Mevcut konumunuzdan Ãļğeleri gÃļrÃŧntÃŧlemek için konum iznine ihtiyaç var. Şimdi izin vermek istiyor musunuz?", "map_no_location_permission_title": "Konum izni reddedildi", "map_settings": "Harita ayarlarÄą", - "map_settings_dark_mode": "Koyu tema", + "map_settings_dark_mode": "Koyu mod", "map_settings_date_range_option_day": "Son 24 saat", "map_settings_date_range_option_days": "Son {days} gÃŧn", "map_settings_date_range_option_year": "Son yÄąl", @@ -1618,7 +1622,7 @@ "no_uploads_in_progress": "YÃŧkleme işlemi yok", "none": "Yok", "not_allowed": "İzin verilmiyor", - "not_available": "YOK", + "not_available": "U/D", "not_in_any_album": "Hiçbir albÃŧmde değil", "not_selected": "Seçilmedi", "notes": "Notlar", @@ -1651,6 +1655,7 @@ "only_favorites": "Sadece favoriler", "open": "Aç", "open_calendar": "Takvimi aç", + "open_in_browser": "TarayÄącÄąda 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ç", @@ -2212,6 +2217,7 @@ "tag": "Etiket", "tag_assets": "Öğeleri etiketle", "tag_created": "Etiket oluşturuldu: {tag}", + "tag_face": "YÃŧzÃŧ etiketle", "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", @@ -2393,6 +2399,7 @@ "viewer_remove_from_stack": "Yığından KaldÄąr", "viewer_stack_use_as_main_asset": "Ana fotoğraf olarak kullan", "viewer_unstack": "YığınÄą KaldÄąr", + "visibility": "GÃļrÃŧnÃŧrlÃŧk", "visibility_changed": "GÃļrÃŧnÃŧrlÃŧk {count, plural, one {# kişi} other {# kişi}} için değiştirildi", "visual": "GÃļrsel", "visual_builder": "GÃļrsel oluşturucu", diff --git a/i18n/uk.json b/i18n/uk.json index 74647210c2..b2b0a01501 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -5,7 +5,7 @@ "acknowledge": "ĐŸŅ€Đ¸ĐšĐŊŅŅ‚Đ¸", "action": "Đ”Ņ–Ņ", "action_common_update": "ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸", - "action_description": "ĐĐ°ĐąŅ–Ņ€ Đ´Ņ–Đš, ŅĐēŅ– ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž виĐēĐžĐŊĐ°Ņ‚Đ¸ С Đ˛Ņ–Đ´Ņ„Ņ–ĐģŅŒŅ‚Ņ€ĐžĐ˛Đ°ĐŊиĐŧи Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", + "action_description": "ĐĐ°ĐąŅ–Ņ€ Đ´Ņ–Đš Đ´ĐģŅ виĐēĐžĐŊаĐŊĐŊŅ ĐŊад Đ˛Ņ–Đ´Ņ„Ņ–ĐģŅŒŅ‚Ņ€ĐžĐ˛Đ°ĐŊиĐŧи ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°Đŧи", "actions": "Đ”Ņ–Ņ—", "active": "АĐēŅ‚Đ¸Đ˛ĐŊиК", "active_count": "АĐēŅ‚Đ¸Đ˛ĐŊŅ–: {count}", @@ -13,18 +13,18 @@ "activity_changed": "АĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ {enabled, select, true {ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž} other {виĐŧĐēĐŊĐĩĐŊĐž}}", "add": "Đ”ĐžĐ´Đ°Ņ‚Đ¸", "add_a_description": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐžĐŋĐ¸Ņ", - "add_a_location": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", + "add_a_location": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐŧҖҁ҆Đĩ", "add_a_name": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Ņ–Đŧ'Ņ", "add_a_title": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐŊĐ°ĐˇĐ˛Ņƒ", "add_action": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Ņ–ŅŽ", "add_action_description": "ĐĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ, Ņ‰ĐžĐą Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Ņ–ŅŽ", - "add_assets": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", + "add_assets": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", "add_birthday": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´ĐĩĐŊҌ ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ", - "add_endpoint": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ°Đ´Ņ€Đĩҁ҃ ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ", - "add_exclusion_pattern": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ виĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ", + "add_endpoint": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ°Đ´Ņ€Đĩҁ҃ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", + "add_exclusion_pattern": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ виĐŊŅŅ‚Đē҃", "add_filter": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ҄ҖĐģŅŒŅ‚Ņ€", "add_filter_description": "ĐĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ, Ņ‰ĐžĐą Đ´ĐžĐ´Đ°Ņ‚Đ¸ ҃ĐŧĐžĐ˛Ņƒ ҄ҖĐģŅŒŅ‚Ņ€Đ°", - "add_location": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", + "add_location": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐŧҖҁ҆Đĩ", "add_more_users": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛", "add_partner": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°", "add_path": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ҈ĐģŅŅ…", @@ -34,113 +34,113 @@ "add_to_album": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", "add_to_album_bottom_sheet_added": "ДодаĐŊĐž Đ´Đž {album}", "add_to_album_bottom_sheet_already_exists": "ВĐļĐĩ Ņ” в {album}", - "add_to_album_bottom_sheet_some_local_assets": "ДĐĩŅĐēŅ– ĐģĐžĐēаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģи ĐŊĐĩ вдаĐģĐžŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", + "add_to_album_bottom_sheet_some_local_assets": "ДĐĩŅĐēŅ– ĐģĐžĐēаĐģҌĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŊĐĩ вдаĐģĐžŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", "add_to_album_toggle": "ПĐĩŅ€ĐĩĐŧиĐēаĐŊĐŊŅ Đ˛Đ¸ĐąĐžŅ€Ņƒ Đ´ĐģŅ {album}", "add_to_albums": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧŅ–Đ˛", "add_to_albums_count": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧŅ–Đ˛ ({count})", "add_to_bottom_bar": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž", "add_to_shared_album": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž аĐģŅŒĐąĐžĐŧ҃", - "add_upload_to_stack": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ в ҁ҂ĐĩĐē", + "add_upload_to_stack": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ виваĐŊŅ‚Đ°ĐļĐĩĐŊиК ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ Đ´Đž ҁ҂ĐĩĐē҃", "add_url": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ URL", - "add_workflow_step": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐēŅ€ĐžĐē Ņ€ĐžĐąĐžŅ‡ĐžĐŗĐž ĐŋŅ€ĐžŅ†Đĩҁ҃", + "add_workflow_step": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐēŅ€ĐžĐē Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–Ņ—", "added_to_archive": "ДодаĐŊĐž Đ´Đž Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ", - "added_to_favorites": "ДодаĐŊĐž Đ´Đž ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž", - "added_to_favorites_count": "ДодаĐŊĐž {count, number} Đ´Đž ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž", + "added_to_favorites": "ДодаĐŊĐž Đ´Đž Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž", + "added_to_favorites_count": "{count, plural, one {ДодаĐŊĐž # ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ Đ´Đž Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž} few {ДодаĐŊĐž # ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ Đ´Đž Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž} many {ДодаĐŊĐž # ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ Đ´Đž Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž} other {ДодаĐŊĐž # ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ Đ´Đž Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž}}", "admin": { - "add_exclusion_pattern_description": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊи виĐēĐģŅŽŅ‡ĐĩĐŊҌ. ĐŸŅ–Đ´ŅŅ‚Đ°ĐŊОвĐēа С виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅĐŧ *, ** Ņ‚Đ° ? ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ”Ņ‚ŅŒŅŅ. ДĐģŅ Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛ŅŅ–Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛ ҃ ĐąŅƒĐ´ŅŒ-ŅĐēĐžĐŧ҃ ĐēĐ°Ņ‚Đ°ĐģĐžĐˇŅ– С Ņ–Đŧ'ŅĐŧ ÂĢRawÂģ, виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ \"**/Raw/**\". ДĐģŅ Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛ŅŅ–Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛, Ņ‰Đž СаĐēŅ–ĐŊŅ‡ŅƒŅŽŅ‚ŅŒŅŅ ĐŊа \".tif\", виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ \"**/*.tif\". ДĐģŅ Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ Đ°ĐąŅĐžĐģŅŽŅ‚ĐŊĐžĐŗĐž ҈ĐģŅŅ…Ņƒ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ \"/path/to/ignore/**\".", + "add_exclusion_pattern_description": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊи виĐŊŅŅ‚ĐēŅ–Đ˛. ĐŸŅ–Đ´ŅŅ‚Đ°ĐŊОвĐēа С виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅĐŧ *, ** Ņ‚Đ° ? ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ”Ņ‚ŅŒŅŅ. ЊОй Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛ŅŅ– Ņ„Đ°ĐšĐģи в ĐąŅƒĐ´ŅŒ-ŅĐēŅ–Đš ĐŋаĐŋ҆Җ С ĐŊĐ°ĐˇĐ˛ĐžŅŽ ÂĢRawÂģ, виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ \"**/Raw/**\". ЊОй Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛ŅŅ– Ņ„Đ°ĐšĐģи, Ņ‰Đž СаĐēŅ–ĐŊŅ‡ŅƒŅŽŅ‚ŅŒŅŅ ĐŊа \".tif\", виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ \"**/*.tif\". ЊОй Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ Đ°ĐąŅĐžĐģŅŽŅ‚ĐŊиК ҈ĐģŅŅ…, виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ \"/path/to/ignore/**\".", "admin_user": "АдĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€", - "asset_offline_description": "ĐĻĐĩĐš Ņ„Đ°ĐšĐģ СОвĐŊŅ–ŅˆĐŊŅŒĐžŅ— ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐŊа Đ´Đ¸ŅĐē҃ Ņ– ĐąŅƒĐ˛ ĐŋĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊиК Đ´Đž ĐēĐžŅˆĐ¸Đēа. Đ¯ĐēŅ‰Đž Ņ„Đ°ĐšĐģ ĐąŅƒĐ˛ ĐŋĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊиК ҃ ĐŧĐĩĐļĐ°Ņ… ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ ŅĐ˛ĐžŅŽ ҁ҂ҀҖ҇Đē҃ ĐŊа ĐŊĐ°ŅĐ˛ĐŊŅ–ŅŅ‚ŅŒ ĐŊĐžĐ˛ĐžĐŗĐž Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´ĐŊĐžĐŗĐž Ņ„Đ°ĐšĐģ҃. ЊОй Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ҆ĐĩĐš Ņ„Đ°ĐšĐģ, ĐŋĐĩŅ€ĐĩĐēĐžĐŊĐ°ĐšŅ‚ĐĩŅŅ, Ņ‰Đž ҈ĐģŅŅ… Đ´Đž Ņ„Đ°ĐšĐģ҃ Đ´ĐžŅŅ‚ŅƒĐŋĐŊиК Đ´ĐģŅ Immich, Ņ– ĐŋŅ€ĐžŅĐēаĐŊŅƒĐšŅ‚Đĩ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃.", - "authentication_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ—", - "authentication_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐ°Ņ€ĐžĐģŅĐŧи, OAuth Ņ‚Đ° Ņ–ĐŊŅˆĐ¸Đŧи ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ—", - "authentication_settings_disable_all": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ виĐŧĐēĐŊŅƒŅ‚Đ¸ Đ˛ŅŅ– ĐŧĐĩŅ‚ĐžĐ´Đ¸ Đ˛Ņ…ĐžĐ´Ņƒ? Đ’Ņ…Ņ–Đ´ ĐąŅƒĐ´Đĩ ĐŋОвĐŊŅ–ŅŅ‚ŅŽ виĐŧĐēĐŊĐĩĐŊиК.", - "authentication_settings_reenable": "ДĐģŅ ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐžĐŗĐž Đ˛Đ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐŊŅ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ КоĐŧаĐŊĐ´Ņƒ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°.", - "background_task_job": "ФОĐŊĐžĐ˛Ņ– ЗавдаĐŊĐŊŅ", + "asset_offline_description": "ĐĻĐĩĐš ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ СОвĐŊŅ–ŅˆĐŊŅŒĐžŅ— ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи ĐąŅ–ĐģҌ҈Đĩ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐŊа Đ´Đ¸ŅĐē҃, Ņ‚ĐžĐŧ҃ ĐšĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа. Đ¯ĐēŅ‰Đž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ ĐąŅƒĐģĐž ĐŋĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž в ĐŧĐĩĐļĐ°Ņ… ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ ŅĐ˛ĐžŅŽ Ņ…Ņ€ĐžĐŊĐžĐģĐžĐŗŅ–ŅŽ ĐŊа ĐŊĐ°ŅĐ˛ĐŊŅ–ŅŅ‚ŅŒ ĐŊĐžĐ˛ĐžĐŗĐž Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´ĐŊĐžĐŗĐž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°. ЊОй Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ҆ĐĩĐš ĐĩĐģĐĩĐŧĐĩĐŊŅ‚, ĐŋĐĩŅ€ĐĩĐēĐžĐŊĐ°ĐšŅ‚ĐĩŅŅ, Ņ‰Đž ҈ĐģŅŅ… Đ´Đž ĐŊŅŒĐžĐŗĐž Đ´ĐžŅŅ‚ŅƒĐŋĐŊиК Đ´ĐģŅ Immich, Ņ– ĐŋŅ€ĐžŅĐēаĐŊŅƒĐšŅ‚Đĩ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃.", + "authentication_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ—", + "authentication_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐ°Ņ€ĐžĐģŅĐŧи, OAuth Ņ‚Đ° Ņ–ĐŊŅˆĐ¸Đŧи ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ—", + "authentication_settings_disable_all": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ виĐŧĐēĐŊŅƒŅ‚Đ¸ Đ˛ŅŅ– ĐŧĐĩŅ‚ĐžĐ´Đ¸ Đ˛Ņ…ĐžĐ´Ņƒ? Đ’Ņ…Ņ–Đ´ ĐąŅƒĐ´Đĩ ĐŋОвĐŊŅ–ŅŅ‚ŅŽ виĐŧĐēĐŊĐĩĐŊĐž.", + "authentication_settings_reenable": "ЊОй ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž ŅƒĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸, виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ КоĐŧаĐŊĐ´Ņƒ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°.", + "background_task_job": "ФОĐŊĐžĐ˛Ņ– СавдаĐŊĐŊŅ", "backup_database": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ даĐŧĐŋ йаСи даĐŊĐ¸Ņ…", "backup_database_enable_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ даĐŧĐŋи йаСи даĐŊĐ¸Ņ…", "backup_keep_last_amount": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅ–Ņ… даĐŧĐŋŅ–Đ˛, ŅĐēŅ– СйĐĩŅ€Ņ–ĐŗĐ°Ņ‚Đ¸", "backup_onboarding_1_description": "Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊа ĐēĐžĐŋŅ–Ņ ҃ Ņ…ĐŧĐ°Ņ€Ņ– айО в Ņ–ĐŊŅˆĐžĐŧ҃ Ņ„Ņ–ĐˇĐ¸Ņ‡ĐŊĐžĐŧ҃ ĐŧҖҁ҆Җ.", - "backup_onboarding_2_description": "ĐģĐžĐēаĐģҌĐŊŅ– ĐēĐžĐŋŅ–Ņ— ĐŊа Ņ€Ņ–ĐˇĐŊĐ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŅ…. ĐĻĐĩ вĐēĐģŅŽŅ‡Đ°Ņ” ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž Ņ– Ņ—Ņ… ĐģĐžĐēаĐģҌĐŊŅ– Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ—.", - "backup_onboarding_3_description": "ĐˇĐ°ĐŗĐ°ĐģҌĐŊŅ– ĐēĐžĐŋŅ–Ņ— Đ˛Đ°ŅˆĐ¸Ņ… даĐŊĐ¸Ņ…, вĐēĐģŅŽŅ‡Đ°ŅŽŅ‡Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž. ĐĻĐĩ вĐēĐģŅŽŅ‡Đ°Ņ” 1 Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊ҃ ĐēĐžĐŋŅ–ŅŽ Ņ– 2 ĐģĐžĐēаĐģҌĐŊŅ– ĐēĐžĐŋŅ–Ņ—.", - "backup_onboarding_description": "Đ ĐĩĐēĐžĐŧĐĩĐŊдОваĐŊĐž Đ´ĐžŅ‚Ņ€Đ¸ĐŧŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ ŅŅ‚Ņ€Đ°Ņ‚ĐĩĐŗŅ–Ņ— Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ 3-2-1 Đ´ĐģŅ ĐˇĐ°Ņ…Đ¸ŅŅ‚Ņƒ Đ˛Đ°ŅˆĐ¸Ņ… даĐŊĐ¸Ņ…. ЗбĐĩŅ€Ņ–ĐŗĐ°ĐšŅ‚Đĩ ĐēĐžĐŋŅ–Ņ— виваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ„ĐžŅ‚Đž Đš Đ˛Ņ–Đ´ĐĩĐž, а Ņ‚Đ°ĐēĐžĐļ йаСи даĐŊĐ¸Ņ… Immich, Ņ‰ĐžĐą СайĐĩСĐŋĐĩŅ‡Đ¸Ņ‚Đ¸ ĐŋОвĐŊĐžŅ†Ņ–ĐŊĐŊиК ĐˇĐ°Ņ…Đ¸ŅŅ‚ Ņ‚Đ° Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ.", + "backup_onboarding_2_description": "ĐģĐžĐēаĐģҌĐŊŅ– ĐēĐžĐŋŅ–Ņ— ĐŊа Ņ€Ņ–ĐˇĐŊĐ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŅ…. ĐĻĐĩ ĐžŅĐŊОвĐŊŅ– Ņ„Đ°ĐšĐģи Ņ‚Đ° Ņ—Ņ… ĐģĐžĐēаĐģҌĐŊŅ– Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ—.", + "backup_onboarding_3_description": "ŅƒŅŅŒĐžĐŗĐž ĐēĐžĐŋŅ–Đš Đ˛Đ°ŅˆĐ¸Ņ… даĐŊĐ¸Ņ…, вĐēĐģŅŽŅ‡ĐŊĐž С ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊиĐŧи Ņ„Đ°ĐšĐģаĐŧи. ĐĻĐĩ 1 Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊа ĐēĐžĐŋŅ–Ņ Ņ‚Đ° 2 ĐģĐžĐēаĐģҌĐŊŅ– ĐēĐžĐŋŅ–Ņ—.", + "backup_onboarding_description": "Đ ĐĩĐēĐžĐŧĐĩĐŊдОваĐŊĐž Đ´ĐžŅ‚Ņ€Đ¸ĐŧŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ ŅŅ‚Ņ€Đ°Ņ‚ĐĩĐŗŅ–Ņ— Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ 3-2-1 Đ´ĐģŅ ĐˇĐ°Ņ…Đ¸ŅŅ‚Ņƒ Đ˛Đ°ŅˆĐ¸Ņ… даĐŊĐ¸Ņ…. ЗбĐĩŅ€Ņ–ĐŗĐ°ĐšŅ‚Đĩ ĐēĐžĐŋŅ–Ņ— виваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ„ĐžŅ‚Đž Đš Đ˛Ņ–Đ´ĐĩĐž, а Ņ‚Đ°ĐēĐžĐļ йаСи даĐŊĐ¸Ņ… Immich, Ņ‰ĐžĐą СайĐĩСĐŋĐĩŅ‡Đ¸Ņ‚Đ¸ ĐŋОвĐŊĐžŅ†Ņ–ĐŊĐŊĐĩ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ.", "backup_onboarding_footer": "ДоĐēĐģадĐŊŅ–ŅˆĐĩ ĐŋŅ€Đž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Immich ĐŧĐžĐļĐŊа Đ´Ņ–ĐˇĐŊĐ°Ņ‚Đ¸ŅŅ С Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ–Ņ—.", - "backup_onboarding_parts_title": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Са ŅŅ‚Ņ€Đ°Ņ‚ĐĩĐŗŅ–Ņ”ŅŽ 3-2-1 вĐēĐģŅŽŅ‡Đ°Ņ”:", + "backup_onboarding_parts_title": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Са ŅŅ‚Ņ€Đ°Ņ‚ĐĩĐŗŅ–Ņ”ŅŽ 3-2-1 ĐžŅ…ĐžĐŋĐģŅŽŅ”:", "backup_onboarding_title": "Đ ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ—", "backup_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ даĐŧĐŋа йаСи даĐŊĐ¸Ņ…", - "backup_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи даĐŧĐŋа йаСи даĐŊĐ¸Ņ….", - "cleared_jobs": "ĐžŅ‡Đ¸Ņ‰ĐĩĐŊŅ– СавдаĐŊĐŊŅ Đ´ĐģŅ: {job}", + "backup_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи даĐŧĐŋа йаСи даĐŊĐ¸Ņ….", + "cleared_jobs": "ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐž СавдаĐŊĐŊŅ Đ´ĐģŅ: {job}", "config_set_by_file": "НаĐģĐ°ŅˆŅ‚ĐžĐ˛Đ°ĐŊĐž Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ ĐēĐžĐŊŅ„Ņ–Đŗ-Ņ„Đ°ĐšĐģ҃", - "confirm_delete_library": "Ви Đ´Ņ–ĐšŅĐŊĐž йаĐļĐ°Ņ”Ņ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃ \"{library}\"?", - "confirm_delete_library_assets": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ Ņ†ŅŽ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃? ĐĻĐĩ ĐąĐĩСĐŋĐžĐ˛ĐžŅ€ĐžŅ‚ĐŊĐž видаĐģĐ¸Ņ‚ŅŒ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} С Immich. ФаКĐģи СаĐģĐ¸ŅˆĐ°Ņ‚ŅŒŅŅ ĐŊа Đ´Đ¸ŅĐē҃.", - "confirm_email_below": "ДĐģŅ ĐŋŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´ĐļĐĩĐŊĐŊŅ ввĐĩĐ´Ņ–Ņ‚ŅŒ \"{email}\" ĐŊиĐļ҇Đĩ", - "confirm_reprocess_all_faces": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž виСĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ Đ˛ŅŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ? ĐĻĐĩ Ņ‚Đ°ĐēĐžĐļ ĐŋŅ€Đ¸ĐˇĐ˛ĐĩĐ´Đĩ Đ´Đž видаĐģĐĩĐŊĐŊŅ Ņ–ĐŧĐĩĐŊ С ŅƒŅŅ–Ņ… ОйĐģĐ¸Ņ‡.", + "confirm_delete_library": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃ ÂĢ{library}Âģ?", + "confirm_delete_library_assets": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ Ņ†ŅŽ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃? ĐĻĐĩ ĐąĐĩСĐŋĐžĐ˛ĐžŅ€ĐžŅ‚ĐŊĐž видаĐģĐ¸Ņ‚ŅŒ {count, plural, one {# ĐŊĐ°ŅĐ˛ĐŊиК ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {ŅƒŅŅ– # ĐŊĐ°ŅĐ˛ĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {ŅƒŅŅ– # ĐŊĐ°ŅĐ˛ĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {ŅƒŅŅ– # ĐŊĐ°ŅĐ˛ĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} С Immich. ФаКĐģи СаĐģĐ¸ŅˆĐ°Ņ‚ŅŒŅŅ ĐŊа Đ´Đ¸ŅĐē҃.", + "confirm_email_below": "ЊОй ĐŋŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´Đ¸Ņ‚Đ¸, ввĐĩĐ´Ņ–Ņ‚ŅŒ ÂĢ{email}Âģ ĐŊиĐļ҇Đĩ", + "confirm_reprocess_all_faces": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž виСĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ Đ˛ŅŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ? ĐĻĐĩ Ņ‚Đ°ĐēĐžĐļ видаĐģĐ¸Ņ‚ŅŒ ŅƒŅŅ–Ņ… Ņ–ĐŧĐĩĐŊОваĐŊĐ¸Ņ… ĐģŅŽĐ´ĐĩĐš.", "confirm_user_password_reset": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ҁĐēиĐŊŅƒŅ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° {user}?", - "confirm_user_pin_code_reset": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ҁĐēиĐŊŅƒŅ‚Đ¸ PIN-ĐēОд {user}?", + "confirm_user_pin_code_reset": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ҁĐēиĐŊŅƒŅ‚Đ¸ PIN-ĐēОд ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° {user}?", "copy_config_to_clipboard_description": "ĐĄĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ ĐŋĐžŅ‚ĐžŅ‡ĐŊ҃ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ ŅĐ¸ŅŅ‚ĐĩĐŧи ŅĐē Ой'Ņ”ĐēŅ‚ JSON ҃ ĐąŅƒŅ„ĐĩŅ€ ОйĐŧŅ–ĐŊ҃", "create_job": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ СавдаĐŊĐŊŅ", - "cron_expression": "Cron Đ˛Đ¸Ņ€Đ°Đˇ", - "cron_expression_description": "Đ’ŅŅ‚Đ°ĐŊĐžĐ˛Ņ–Ņ‚ŅŒ Ņ–ĐŊŅ‚ĐĩŅ€Đ˛Đ°Đģ ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ– cron. Đ”ĐžĐ´Đ°Ņ‚ĐēОва Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ: Crontab Guru", - "cron_expression_presets": "ПоĐŋĐĩŅ€ĐĩĐ´ĐŊŅ– ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ cron Đ˛Đ¸Ņ€Đ°ĐˇŅ–Đ˛", + "cron_expression": "Cron-Đ˛Đ¸Ņ€Đ°Đˇ", + "cron_expression_description": "ĐŖŅŅ‚Đ°ĐŊĐžĐ˛Ņ–Ņ‚ŅŒ Ņ–ĐŊŅ‚ĐĩŅ€Đ˛Đ°Đģ ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ– cron. Đ”ĐžĐ´Đ°Ņ‚ĐēОва Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ: Crontab Guru", + "cron_expression_presets": "ШайĐģĐžĐŊи Cron-Đ˛Đ¸Ņ€Đ°ĐˇŅ–Đ˛", "disable_login": "ВиĐŧĐēĐŊŅƒŅ‚Đ¸ Đ˛Ņ…Ņ–Đ´", - "duplicate_detection_job_description": "ЗаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐĩ ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ Đ´ĐģŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ŅŅ…ĐžĐļĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ. ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ” Ņ–ĐŊŅ‚ĐĩĐģĐĩĐēŅ‚ŅƒĐ°ĐģҌĐŊиК ĐŋĐžŅˆŅƒĐē", - "exclusion_pattern_description": "ШайĐģĐžĐŊи виĐēĐģŅŽŅ‡ĐĩĐŊҌ дОСвОĐģŅŅŽŅ‚ŅŒ Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи Ņ‚Đ° ĐŋаĐŋĐēи ĐŋŅ–Đ´ Ņ‡Đ°Ņ ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛Đ°ŅˆĐžŅ— ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи. ĐĻĐĩ ĐēĐžŅ€Đ¸ŅĐŊĐž, ŅĐēŅ‰Đž ҃ Đ˛Đ°Ņ Ņ” ĐŋаĐŋĐēи, ŅĐēŅ– ĐŧŅ–ŅŅ‚ŅŅ‚ŅŒ Ņ„Đ°ĐšĐģи, ŅĐēŅ– ви ĐŊĐĩ Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Ņ–ĐŧĐŋĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸, ĐŊаĐŋŅ€Đ¸ĐēĐģад, RAW-Ņ„Đ°ĐšĐģи.", + "duplicate_detection_job_description": "ВиĐēĐžĐŊĐ°Ņ‚Đ¸ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐĩ ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ Đ´ĐģŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ŅŅ…ĐžĐļĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ. ĐŸĐžŅ‚Ņ€ĐĩĐąŅƒŅ” Ņ€ĐžĐˇŅƒĐŧĐŊĐžĐŗĐž ĐŋĐžŅˆŅƒĐē҃", + "exclusion_pattern_description": "ШайĐģĐžĐŊи виĐŊŅŅ‚ĐēŅ–Đ˛ Đ´Đ°ŅŽŅ‚ŅŒ СĐŧĐžĐŗŅƒ Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи Ņ‚Đ° ĐŋаĐŋĐēи ĐŋŅ–Đ´ Ņ‡Đ°Ņ ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи. ĐĻĐĩ ĐēĐžŅ€Đ¸ŅĐŊĐž Đ´ĐģŅ ĐŋаĐŋĐžĐē Ņ–Đˇ ĐŊĐĩйаĐļаĐŊиĐŧи Đ´ĐģŅ Ņ–ĐŧĐŋĐžŅ€Ņ‚Ņƒ Ņ„Đ°ĐšĐģаĐŧи, ĐŊаĐŋŅ€Đ¸ĐēĐģад Ņ„Đ°ĐšĐģаĐŧи RAW.", "export_config_as_json_description": "ЗаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐŋĐžŅ‚ĐžŅ‡ĐŊ҃ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ ŅĐ¸ŅŅ‚ĐĩĐŧи ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ– JSON", "external_libraries_page_description": "ĐĄŅ‚ĐžŅ€Ņ–ĐŊĐēа СОвĐŊŅ–ŅˆĐŊŅŒĐžŅ— ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", - "face_detection": "Đ’Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡Ņ‡Ņ", - "face_detection_description": "Đ’Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡ ĐŊа ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅŅ… Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ. ДĐģŅ Đ˛Ņ–Đ´ĐĩĐž ĐžĐąŅ€ĐžĐąĐģŅŅ”Ņ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ°. \\\"ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸\\\" ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž ĐžĐąŅ€ĐžĐąĐģŅŅ” Đ˛ŅŅ– ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ. \\\"ĐĄĐēиĐŊŅƒŅ‚Đ¸\\\" Đ´ĐžĐ´Đ°Ņ‚ĐēОвО ĐžŅ‡Đ¸Ņ‰Đ°Ņ” Đ˛ŅŅ– ĐŋĐžŅ‚ĐžŅ‡ĐŊŅ– даĐŊŅ– ĐŋŅ€Đž ОйĐģĐ¸Ņ‡Ņ‡Ņ. \\\"Đ’Ņ–Đ´ŅŅƒŅ‚ĐŊŅ–\\\" ŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ ҃ ҇ĐĩŅ€ĐŗŅƒ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ, ŅĐēŅ– ҉Đĩ ĐŊĐĩ ĐąŅƒĐģи ĐžĐąŅ€ĐžĐąĐģĐĩĐŊŅ–. Đ’Đ¸ŅĐ˛ĐģĐĩĐŊŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐŋĐžŅŅ‚Đ°Đ˛ĐģĐĩĐŊŅ– в ҇ĐĩŅ€ĐŗŅƒ Đ´ĐģŅ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ ĐŋҖҁĐģŅ СавĐĩŅ€ŅˆĐĩĐŊĐŊŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ, ĐŗŅ€ŅƒĐŋŅƒŅŽŅ‡Đ¸ Ņ—Ņ… ҃ вĐļĐĩ ҖҁĐŊŅƒŅŽŅ‡Đ¸Ņ… айО ĐŊĐžĐ˛Đ¸Ņ… ĐģŅŽĐ´ĐĩĐš.", - "facial_recognition_job_description": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐ¸Ņ… ОйĐģĐ¸Ņ‡ ҃ ĐģŅŽĐ´ĐĩĐš. ĐĻĐĩĐš ĐēŅ€ĐžĐē виĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ ĐŋҖҁĐģŅ СавĐĩŅ€ŅˆĐĩĐŊĐŊŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡. \"ĐĄĐēиĐŊŅƒŅ‚Đ¸\" ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž ĐēĐģĐ°ŅŅ‚ĐĩŅ€Đ¸ĐˇŅƒŅ” Đ˛ŅŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ. \"Đ’Ņ–Đ´ŅŅƒŅ‚ĐŊŅ–\" ŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ ҃ ҇ĐĩŅ€ĐŗŅƒ ОйĐģĐ¸Ņ‡Ņ‡Ņ, ŅĐēиĐŧ ҉Đĩ ĐŊĐĩ ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐž ĐģŅŽĐ´Đ¸ĐŊ҃.", - "failed_job_command": "КоĐŧаĐŊда {command} ĐŊĐĩ виĐēĐžĐŊаĐģĐ°ŅŅ Đ´ĐģŅ СавдаĐŊĐŊŅ: {job}", - "force_delete_user_warning": "ĐŸĐžĐŸĐ•Đ Đ•Đ”Đ–Đ•ĐĐĐ¯: ĐĻĐĩ ĐŊĐĩĐŗĐ°ĐšĐŊĐž ĐŋŅ€Đ¸ĐˇĐ˛ĐĩĐ´Đĩ Đ´Đž видаĐģĐĩĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Ņ– Đ˛ŅŅ–Ņ… ĐšĐžĐŗĐž Ņ„Đ°ĐšĐģŅ–Đ˛. ĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸, Ņ– Ņ„Đ°ĐšĐģи ĐŊĐĩ ĐŧĐžĐļĐŊа ĐąŅƒĐ´Đĩ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸.", + "face_detection": "Đ’Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡", + "face_detection_description": "Đ’Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡ ĐŊа ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°Ņ… Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ. ДĐģŅ Đ˛Ņ–Đ´ĐĩĐž ĐžĐąŅ€ĐžĐąĐģŅŅ”Ņ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ°. ÂĢОĐŊĐžĐ˛Đ¸Ņ‚Đ¸Âģ ĐžĐąŅ€ĐžĐąĐģŅŅ” (айО ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž ĐžĐąŅ€ĐžĐąĐģŅŅ”) Đ˛ŅŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸. ÂĢĐĄĐēиĐŊŅƒŅ‚Đ¸Âģ Đ´ĐžĐ´Đ°Ņ‚ĐēОвО ĐžŅ‡Đ¸Ņ‰Đ°Ņ” Đ˛ŅŅ– ĐŋĐžŅ‚ĐžŅ‡ĐŊŅ– даĐŊŅ– ĐŋŅ€Đž ОйĐģĐ¸Ņ‡Ņ‡Ņ. ÂĢĐ’Ņ–Đ´ŅŅƒŅ‚ĐŊŅ–Âģ ŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ ҃ ҇ĐĩŅ€ĐŗŅƒ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸, ŅĐēŅ– ҉Đĩ ĐŊĐĩ ĐąŅƒĐģĐž ĐžĐąŅ€ĐžĐąĐģĐĩĐŊĐž. Đ’Đ¸ŅĐ˛ĐģĐĩĐŊŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ ĐąŅƒĐ´Đĩ ĐŋĐžŅŅ‚Đ°Đ˛ĐģĐĩĐŊĐž в ҇ĐĩŅ€ĐŗŅƒ Đ´ĐģŅ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ ĐŋҖҁĐģŅ СавĐĩŅ€ŅˆĐĩĐŊĐŊŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ, ĐŋŅ€Đ¸Ņ”Đ´ĐŊŅƒŅŽŅ‡Đ¸ Ņ—Ņ… Đ´Đž ĐŊĐ°ŅĐ˛ĐŊĐ¸Ņ… айО ĐŊĐžĐ˛Đ¸Ņ… ĐģŅŽĐ´ĐĩĐš.", + "facial_recognition_job_description": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐ¸Ņ… ОйĐģĐ¸Ņ‡ ҃ ĐģŅŽĐ´ĐĩĐš. ĐĻĐĩĐš ĐēŅ€ĐžĐē виĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ ĐŋҖҁĐģŅ СавĐĩŅ€ŅˆĐĩĐŊĐŊŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡. ÂĢĐĄĐēиĐŊŅƒŅ‚Đ¸Âģ ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž ĐēĐģĐ°ŅŅ‚ĐĩŅ€Đ¸ĐˇŅƒŅ” Đ˛ŅŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ. ÂĢĐ’Ņ–Đ´ŅŅƒŅ‚ĐŊŅ–Âģ ŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ ҃ ҇ĐĩŅ€ĐŗŅƒ ОйĐģĐ¸Ņ‡Ņ‡Ņ, ŅĐēиĐŧ ҉Đĩ ĐŊĐĩ ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐž ĐģŅŽĐ´Đ¸ĐŊ҃.", + "failed_job_command": "НĐĩ вдаĐģĐžŅŅ виĐēĐžĐŊĐ°Ņ‚Đ¸ ĐēĐžĐŧаĐŊĐ´Ņƒ {command} Đ´ĐģŅ СавдаĐŊĐŊŅ: {job}", + "force_delete_user_warning": "ĐŸĐžĐŸĐ•Đ Đ•Đ”Đ–Đ•ĐĐĐ¯: ĐĻĐĩ ĐŊĐĩĐŗĐ°ĐšĐŊĐž видаĐģĐ¸Ņ‚ŅŒ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Ņ‚Đ° Đ˛ŅŅ– ĐšĐžĐŗĐž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸. ĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸, Ņ– Ņ„Đ°ĐšĐģи ĐŊĐĩ ĐŧĐžĐļĐŊа ĐąŅƒĐ´Đĩ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸.", "image_format": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚", - "image_format_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚ WebP Đ˛Đ¸Ņ€ĐžĐąĐģŅŅ” ĐŧĐĩĐŊŅˆŅ– Ņ„Đ°ĐšĐģи, ĐŊŅ–Đļ JPEG, аĐģĐĩ ĐšĐžĐŗĐž ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ виĐŧĐ°ĐŗĐ°Ņ” ĐąŅ–ĐģҌ҈Đĩ Ņ‡Đ°ŅŅƒ.", - "image_fullsize_description": "ПовĐŊĐžŅ€ĐžĐˇĐŧŅ–Ņ€ĐŊĐĩ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ С видаĐģĐĩĐŊиĐŧи ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊиĐŧи, ŅĐēŅ– виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅŽŅ‚ŅŒŅŅ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐˇĐąŅ–ĐģҌ҈ĐĩĐŊĐŊŅ", + "image_format_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚ WebP ŅŅ‚Đ˛ĐžŅ€ŅŽŅ” ĐŧĐĩĐŊŅˆŅ– Ņ„Đ°ĐšĐģи, ĐŊŅ–Đļ JPEG, аĐģĐĩ ĐēĐžĐ´ŅƒŅ”Ņ‚ŅŒŅŅ ĐŋĐžĐ˛Ņ–ĐģҌĐŊŅ–ŅˆĐĩ.", + "image_fullsize_description": "ПовĐŊĐžŅ€ĐžĐˇĐŧŅ–Ņ€ĐŊĐĩ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ С видаĐģĐĩĐŊиĐŧи ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊиĐŧи, Ņ‰Đž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐˇĐąŅ–ĐģҌ҈ĐĩĐŊĐŊŅ", "image_fullsize_enabled": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŋОвĐŊĐžŅ€ĐžĐˇĐŧŅ–Ņ€ĐŊĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", - "image_fullsize_enabled_description": "ГĐĩĐŊĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋОвĐŊĐžĐŗĐž Ņ€ĐžĐˇĐŧŅ–Ņ€Ņƒ Đ´ĐģŅ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ–Đ˛, ĐŊĐĩ ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐ¸Ņ… Đ´ĐģŅ вĐĩĐąŅƒ. Đ¯ĐēŅ‰Đž ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž \"ĐĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊĐžĐŧ҃ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŧ҃ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ\", Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊŅ– ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅ– ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ¸ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅŽŅ‚ŅŒŅŅ ĐąĐĩС ĐēĐžĐŊвĐĩŅ€Ņ‚Đ°Ņ†Ņ–Ņ—. НĐĩ вĐŋĐģĐ¸Đ˛Đ°Ņ” ĐŊа вĐĩĐą-Đ´Ņ€ŅƒĐļĐŊŅ– Ņ„ĐžŅ€ĐŧĐ°Ņ‚Đ¸, Ņ‚Đ°ĐēŅ– ŅĐē JPEG.", - "image_fullsize_quality_description": "Đ¯ĐēŅ–ŅŅ‚ŅŒ ĐŋОвĐŊĐžŅ€ĐžĐˇĐŧŅ–Ņ€ĐŊĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ Đ˛Ņ–Đ´ 1 Đ´Đž 100. ЧиĐŧ Đ˛Đ¸Ņ‰Đĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ, Ņ‚Đ¸Đŧ ĐēŅ€Đ°Ņ‰Đĩ ŅĐēŅ–ŅŅ‚ŅŒ, аĐģĐĩ ĐąŅ–ĐģҌ҈Đĩ Ņ€ĐžĐˇĐŧŅ–Ņ€ Ņ„Đ°ĐšĐģ҃.", + "image_fullsize_enabled_description": "ĐĄŅ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋОвĐŊĐžĐŗĐž Ņ€ĐžĐˇĐŧŅ–Ņ€Ņƒ Đ´ĐģŅ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ–Đ˛, ĐŊĐĩ ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐ¸Ņ… Đ´ĐģŅ вĐĩĐąŅƒ. Đ¯ĐēŅ‰Đž ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž ÂĢĐĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊĐžĐŧ҃ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŧ҃ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´ŅƒÂģ, Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊŅ– ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅ– ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ¸ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅŽŅ‚ŅŒŅŅ ĐąĐĩС ĐŋĐĩŅ€ĐĩŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ. НĐĩ вĐŋĐģĐ¸Đ˛Đ°Ņ” ĐŊа Ņ„ĐžŅ€ĐŧĐ°Ņ‚Đ¸, ҁ҃ĐŧҖҁĐŊŅ– С вĐĩйОĐŧ, Ņ‚Đ°ĐēŅ– ŅĐē JPEG.", + "image_fullsize_quality_description": "Đ¯ĐēŅ–ŅŅ‚ŅŒ ĐŋОвĐŊĐžŅ€ĐžĐˇĐŧŅ–Ņ€ĐŊĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ Đ˛Ņ–Đ´ 1 Đ´Đž 100. ЧиĐŧ Đ˛Đ¸Ņ‰Đĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ, Ņ‚Đ¸Đŧ ĐēŅ€Đ°Ņ‰Đ° ŅĐēŅ–ŅŅ‚ŅŒ, аĐģĐĩ ĐąŅ–ĐģŅŒŅˆĐ¸Đš Ņ€ĐžĐˇĐŧŅ–Ņ€ Ņ„Đ°ĐšĐģ҃.", "image_fullsize_title": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋОвĐŊĐžŅ€ĐžĐˇĐŧŅ–Ņ€ĐŊĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "image_prefer_embedded_preview": "ĐĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊĐžĐŧ҃ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŧ҃ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ", - "image_prefer_embedded_preview_setting_description": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊŅ– ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅ– ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ¸ в RAW-Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–ŅŅ… ŅĐē Đ˛Ņ…Ņ–Đ´ĐŊŅ– даĐŊŅ– Đ´ĐģŅ ĐžĐąŅ€ĐžĐąĐēи ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ, ŅĐēŅ‰Đž вОĐŊи Đ´ĐžŅŅ‚ŅƒĐŋĐŊŅ–. ĐĻĐĩ ĐŧĐžĐļĐĩ СайĐĩСĐŋĐĩŅ‡Đ¸Ņ‚Đ¸ Ņ‚ĐžŅ‡ĐŊŅ–ŅˆŅ– ĐēĐžĐģŅŒĐžŅ€Đ¸ Đ´ĐģŅ Đ´ĐĩŅĐēĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ, аĐģĐĩ ŅĐēŅ–ŅŅ‚ŅŒ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ СаĐģĐĩĐļĐ¸Ņ‚ŅŒ Đ˛Ņ–Đ´ ĐēаĐŧĐĩŅ€Đ¸ Ņ– ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ ĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ Đ°Ņ€Ņ‚ĐĩŅ„Đ°ĐēŅ‚Ņ–Đ˛ ŅŅ‚Đ¸ŅĐŊĐĩĐŊĐŊŅ.", - "image_prefer_wide_gamut": "Đ’Ņ–Đ´Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ ŅˆĐ¸Ņ€ĐžĐēŅ–Đš ĐŗĐ°ĐŧŅ–", - "image_prefer_wide_gamut_setting_description": "ДĐģŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ Đ´Đ¸ŅĐŋĐģĐĩĐš P3. ĐĻĐĩ ĐēŅ€Đ°Ņ‰Đĩ СйĐĩŅ€Ņ–ĐŗĐ°Ņ” ŅŅĐēŅ€Đ°Đ˛Ņ–ŅŅ‚ŅŒ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ С ŅˆĐ¸Ņ€ĐžĐēиĐŧ ĐēĐžĐģŅ–Ņ€ĐŊиĐŧ ĐŋŅ€ĐžŅŅ‚ĐžŅ€ĐžĐŧ, аĐģĐĩ ĐŊа ŅŅ‚Đ°Ņ€Đ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŅ… ĐˇŅ– ŅŅ‚Đ°Ņ€ĐžŅŽ вĐĩŅ€ŅŅ–Ņ”ŅŽ ĐąŅ€Đ°ŅƒĐˇĐĩŅ€Đ° ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŧĐžĐļŅƒŅ‚ŅŒ Đ˛Đ¸ĐŗĐģŅĐ´Đ°Ņ‚Đ¸ Ņ–ĐŊаĐē҈Đĩ. sRGB-ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ СйĐĩŅ€Ņ–ĐŗĐ°ŅŽŅ‚ŅŒŅŅ ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ– sRGB, Ņ‰ĐžĐą ҃ĐŊиĐēĐŊŅƒŅ‚Đ¸ ĐˇŅŅƒĐ˛Ņƒ ĐēĐžĐģŅŒĐžŅ€Ņ–Đ˛.", - "image_preview_description": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ҁĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž Ņ€ĐžĐˇĐŧŅ–Ņ€Ņƒ ĐąĐĩС ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ…, ŅĐēĐĩ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ ĐŋŅ€Đ¸ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņ– ĐžĐēŅ€ĐĩĐŧĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ Ņ‚Đ° Đ´ĐģŅ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ", - "image_preview_quality_description": "Đ¯ĐēŅ–ŅŅ‚ŅŒ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ Đ˛Ņ–Đ´ 1 Đ´Đž 100. Đ’Đ¸Ņ‰Đ° ĐžŅ†Ņ–ĐŊĐēа ОСĐŊĐ°Ņ‡Đ°Ņ” ĐēŅ€Đ°Ņ‰Ņƒ ŅĐēŅ–ŅŅ‚ŅŒ, аĐģĐĩ ŅŅ‚Đ˛ĐžŅ€ŅŽŅ” ĐąŅ–ĐģŅŒŅˆŅ– Ņ„Đ°ĐšĐģи Ņ‚Đ° ĐŧĐžĐļĐĩ СĐŧĐĩĐŊŅˆĐ¸Ņ‚Đ¸ ŅˆĐ˛Đ¸Đ´ĐēŅ–ŅŅ‚ŅŒ Ņ€ĐžĐąĐžŅ‚Đ¸ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃. ĐĐ¸ĐˇŅŒĐēĐĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ вĐŋĐģиĐŊŅƒŅ‚Đ¸ ĐŊа ŅĐēŅ–ŅŅ‚ŅŒ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ.", + "image_prefer_embedded_preview_setting_description": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊŅ– ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅ– ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ¸ в RAW-Ņ„ĐžŅ‚Đž ŅĐē Đ˛Ņ…Ņ–Đ´ĐŊŅ– даĐŊŅ– Đ´ĐģŅ ĐžĐąŅ€ĐžĐąĐēи ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ, ŅĐēŅ‰Đž вОĐŊи Đ´ĐžŅŅ‚ŅƒĐŋĐŊŅ–. ĐĻĐĩ ĐŧĐžĐļĐĩ СайĐĩСĐŋĐĩŅ‡Đ¸Ņ‚Đ¸ Ņ‚ĐžŅ‡ĐŊŅ–ŅˆŅ– ĐēĐžĐģŅŒĐžŅ€Đ¸ Đ´ĐģŅ Đ´ĐĩŅĐēĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ, аĐģĐĩ ŅĐēŅ–ŅŅ‚ŅŒ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ СаĐģĐĩĐļĐ¸Ņ‚ŅŒ Đ˛Ņ–Đ´ ĐēаĐŧĐĩŅ€Đ¸ Ņ– ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ ĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ Đ°Ņ€Ņ‚ĐĩŅ„Đ°ĐēŅ‚Ņ–Đ˛ ŅŅ‚Đ¸ŅĐŊĐĩĐŊĐŊŅ.", + "image_prefer_wide_gamut": "ĐĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ ŅˆĐ¸Ņ€ĐžĐēŅ–Đš ĐŗĐ°ĐŧŅ–", + "image_prefer_wide_gamut_setting_description": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐēĐžĐģŅ–Ņ€ĐŊиК ĐŋŅ€ĐžŅŅ‚Ņ–Ņ€ Display P3 Đ´ĐģŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€. ĐĻĐĩ ĐēŅ€Đ°Ņ‰Đĩ СйĐĩŅ€Ņ–ĐŗĐ°Ņ” ĐŊĐ°ŅĐ¸Ņ‡ĐĩĐŊŅ–ŅŅ‚ŅŒ ĐēĐžĐģŅŒĐžŅ€Ņ–Đ˛ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ Ņ–Đˇ ŅˆĐ¸Ņ€ĐžĐēиĐŧ ĐēĐžĐģŅ–Ņ€ĐŊиĐŧ ĐŋŅ€ĐžŅŅ‚ĐžŅ€ĐžĐŧ, аĐģĐĩ ĐŊа ĐˇĐ°ŅŅ‚Đ°Ņ€Ņ–ĐģĐ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŅ… Ņ–Đˇ давĐŊŅŒĐžŅŽ вĐĩŅ€ŅŅ–Ņ”ŅŽ ĐąŅ€Đ°ŅƒĐˇĐĩŅ€Đ° ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŧĐžĐļŅƒŅ‚ŅŒ Đ˛Đ¸ĐŗĐģŅĐ´Đ°Ņ‚Đ¸ Ņ–ĐŊаĐē҈Đĩ. sRGB-ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ СйĐĩŅ€Ņ–ĐŗĐ°ŅŽŅ‚ŅŒŅŅ ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ– sRGB, Ņ‰ĐžĐą ҃ĐŊиĐēĐŊŅƒŅ‚Đ¸ ĐˇŅŅƒĐ˛Ņƒ ĐēĐžĐģŅŒĐžŅ€Ņ–Đ˛.", + "image_preview_description": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ҁĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž Ņ€ĐžĐˇĐŧŅ–Ņ€Ņƒ ĐąĐĩС ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ…, ŅĐēĐĩ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ ĐžĐēŅ€ĐĩĐŧĐžĐŗĐž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ° Ņ‚Đ° Đ´ĐģŅ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ", + "image_preview_quality_description": "Đ¯ĐēŅ–ŅŅ‚ŅŒ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ Đ˛Ņ–Đ´ 1 Đ´Đž 100. Đ’Đ¸Ņ‰Đĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ОСĐŊĐ°Ņ‡Đ°Ņ” ĐēŅ€Đ°Ņ‰Ņƒ ŅĐēŅ–ŅŅ‚ŅŒ, аĐģĐĩ ŅŅ‚Đ˛ĐžŅ€ŅŽŅ” ĐąŅ–ĐģŅŒŅˆŅ– Ņ„Đ°ĐšĐģи Ņ‚Đ° ĐŧĐžĐļĐĩ СĐŧĐĩĐŊŅˆĐ¸Ņ‚Đ¸ ŅˆĐ˛Đ¸Đ´ĐēŅ–ŅŅ‚ŅŒ Ņ€ĐžĐąĐžŅ‚Đ¸ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃. ĐĐ¸ĐˇŅŒĐēĐĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ вĐŋĐģиĐŊŅƒŅ‚Đ¸ ĐŊа ŅĐēŅ–ŅŅ‚ŅŒ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ.", "image_preview_title": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ", "image_progressive": "ĐŸŅ€ĐžĐŗŅ€ĐĩŅĐ¸Đ˛ĐŊиК", - "image_progressive_description": "ĐšĐžĐ´ŅƒĐšŅ‚Đĩ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ JPEG ĐŋĐžŅŅ‚ŅƒĐŋОвО Đ´ĐģŅ ĐŋĐžŅŅ‚ŅƒĐŋĐžĐ˛ĐžĐŗĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ. ĐĻĐĩ ĐŊĐĩ вĐŋĐģĐ¸Đ˛Đ°Ņ” ĐŊа ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ WebP.", + "image_progressive_description": "ĐšĐžĐ´ŅƒĐ˛Đ°Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ JPEG ĐŋŅ€ĐžĐŗŅ€ĐĩŅĐ¸Đ˛ĐŊĐž Đ´ĐģŅ ĐŋĐžŅŅ‚ŅƒĐŋĐžĐ˛ĐžĐŗĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ. ĐĻĐĩ ĐŊĐĩ вĐŋĐģĐ¸Đ˛Đ°Ņ” ĐŊа ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ WebP.", "image_quality": "Đ¯ĐēŅ–ŅŅ‚ŅŒ", "image_resolution": "Đ ĐžĐˇĐ´Ņ–ĐģҌĐŊа ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ", "image_resolution_description": "Đ’Đ¸Ņ‰Đ° Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊа ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ ĐŧĐžĐļĐĩ СйĐĩŅ€Ņ–ĐŗĐ°Ņ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ Đ´ĐĩŅ‚Đ°ĐģĐĩĐš, аĐģĐĩ СаКĐŧĐ°Ņ” ĐąŅ–ĐģҌ҈Đĩ Ņ‡Đ°ŅŅƒ Đ´ĐģŅ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ, ĐŧĐ°Ņ” ĐąŅ–ĐģŅŒŅˆŅ– Ņ€ĐžĐˇĐŧŅ–Ņ€Đ¸ Ņ„Đ°ĐšĐģŅ–Đ˛ Ņ– ĐŧĐžĐļĐĩ СĐŧĐĩĐŊŅˆĐ¸Ņ‚Đ¸ ŅˆĐ˛Đ¸Đ´ĐēŅ–ŅŅ‚ŅŒ Ņ€ĐžĐąĐžŅ‚Đ¸ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃.", "image_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", - "image_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ŅĐēŅ–ŅŅ‚ŅŽ Ņ‚Đ° Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅŽ ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŽ ĐˇĐŗĐĩĐŊĐĩŅ€ĐžĐ˛Đ°ĐŊĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ", - "image_thumbnail_description": "МаĐģĐĩĐŊҌĐēа ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ° Ņ–Đˇ видаĐģĐĩĐŊиĐŧи ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊиĐŧи, Ņ‰Đž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ Đ´ĐģŅ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ ĐŗŅ€ŅƒĐŋ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš, ĐŊаĐŋŅ€Đ¸ĐēĐģад, ĐŊа ĐžŅĐŊОвĐŊŅ–Đš ĐģŅ–ĐŊŅ–Ņ— Ņ‡Đ°ŅŅƒ", - "image_thumbnail_quality_description": "Đ¯ĐēŅ–ŅŅ‚ŅŒ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Đ˛Ņ–Đ´ 1 Đ´Đž 100. Đ’Đ¸Ņ‰Đ° ĐžŅ†Ņ–ĐŊĐēа ОСĐŊĐ°Ņ‡Đ°Ņ” ĐēŅ€Đ°Ņ‰Ņƒ ŅĐēŅ–ŅŅ‚ŅŒ, аĐģĐĩ ŅŅ‚Đ˛ĐžŅ€ŅŽŅ” ĐąŅ–ĐģŅŒŅˆŅ– Ņ„Đ°ĐšĐģи Ņ‚Đ° ĐŧĐžĐļĐĩ СĐŧĐĩĐŊŅˆĐ¸Ņ‚Đ¸ ŅˆĐ˛Đ¸Đ´ĐēŅ–ŅŅ‚ŅŒ Ņ€ĐžĐąĐžŅ‚Đ¸ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃.", + "image_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ŅĐēŅ–ŅŅ‚ŅŽ Ņ‚Đ° Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅŽ ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŽ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ", + "image_thumbnail_description": "МаĐģĐĩĐŊҌĐēа ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ° Ņ–Đˇ видаĐģĐĩĐŊиĐŧи ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊиĐŧи, Ņ‰Đž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ Đ´ĐģŅ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ ĐŗŅ€ŅƒĐŋ Ņ„ĐžŅ‚Đž, ĐŊаĐŋŅ€Đ¸ĐēĐģад, ĐŊа ĐžŅĐŊОвĐŊŅ–Đš Ņ…Ņ€ĐžĐŊĐžĐģĐžĐŗŅ–Ņ—", + "image_thumbnail_quality_description": "Đ¯ĐēŅ–ŅŅ‚ŅŒ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Đ˛Ņ–Đ´ 1 Đ´Đž 100. Đ’Đ¸Ņ‰Đĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ОСĐŊĐ°Ņ‡Đ°Ņ” ĐēŅ€Đ°Ņ‰Ņƒ ŅĐēŅ–ŅŅ‚ŅŒ, аĐģĐĩ ŅŅ‚Đ˛ĐžŅ€ŅŽŅ” ĐąŅ–ĐģŅŒŅˆŅ– Ņ„Đ°ĐšĐģи Ņ‚Đ° ĐŧĐžĐļĐĩ СĐŧĐĩĐŊŅˆĐ¸Ņ‚Đ¸ ŅˆĐ˛Đ¸Đ´ĐēŅ–ŅŅ‚ŅŒ Ņ€ĐžĐąĐžŅ‚Đ¸ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃.", "image_thumbnail_title": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€", - "import_config_from_json_description": "ІĐŧĐŋĐžŅ€Ņ‚ŅƒĐšŅ‚Đĩ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ ŅĐ¸ŅŅ‚ĐĩĐŧи, виваĐŊŅ‚Đ°ĐļĐ¸Đ˛ŅˆĐ¸ Ņ„Đ°ĐšĐģ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–Ņ— JSON", - "job_concurrency": "{job} ОдĐŊĐžŅ‡Đ°ŅĐŊĐž", + "import_config_from_json_description": "ІĐŧĐŋĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ ŅĐ¸ŅŅ‚ĐĩĐŧи, виваĐŊŅ‚Đ°ĐļĐ¸Đ˛ŅˆĐ¸ Ņ„Đ°ĐšĐģ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–Ņ— JSON", + "job_concurrency": "ĐŸĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊŅ–ŅŅ‚ŅŒ {job}", "job_created": "ЗавдаĐŊĐŊŅ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž", "job_not_concurrency_safe": "ĐĻĐĩ СавдаĐŊĐŊŅ ĐŊĐĩ Ņ” ĐąĐĩСĐŋĐĩ҇ĐŊиĐŧ Đ´ĐģŅ ОдĐŊĐžŅ‡Đ°ŅĐŊĐžĐŗĐž виĐēĐžĐŊаĐŊĐŊŅ.", "job_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ СавдаĐŊҌ", "job_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊŅ–ŅŅ‚ŅŽ СавдаĐŊҌ", - "jobs_delayed": "{jobCount, plural, other {# Đ˛Ņ–Đ´ĐēĐģадĐĩĐŊĐž}}", - "jobs_failed": "{jobCount, plural, other {# ĐŊĐĩ вдаĐģĐžŅŅ}}", + "jobs_delayed": "{jobCount, plural, one {# СавдаĐŊĐŊŅ Đ˛Ņ–Đ´ĐēĐģадĐĩĐŊĐž} few {# СавдаĐŊĐŊŅ Đ˛Ņ–Đ´ĐēĐģадĐĩĐŊĐž} many {# СавдаĐŊҌ Đ˛Ņ–Đ´ĐēĐģадĐĩĐŊĐž} other {# СавдаĐŊҌ Đ˛Ņ–Đ´ĐēĐģадĐĩĐŊĐž}}", + "jobs_failed": "{jobCount, plural, one {# СавдаĐŊĐŊŅ ĐŊĐĩ вдаĐģĐžŅŅ} few {# СавдаĐŊĐŊŅ ĐŊĐĩ вдаĐģĐžŅŅ} many {# СавдаĐŊҌ ĐŊĐĩ вдаĐģĐžŅŅ} other {# СавдаĐŊҌ ĐŊĐĩ вдаĐģĐžŅŅ}}", "jobs_over_time": "ЗавдаĐŊĐŊŅ Са Ņ‡Đ°ŅĐžĐŧ", - "library_created": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊа ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēа: {library}", + "library_created": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃: {library}", "library_deleted": "Đ‘Ņ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃ видаĐģĐĩĐŊĐž", "library_details": "ДĐĩŅ‚Đ°ĐģŅ– ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", - "library_folder_description": "ВĐēаĐļŅ–Ņ‚ŅŒ ĐŋаĐŋĐē҃ Đ´ĐģŅ Ņ–ĐŧĐŋĐžŅ€Ņ‚Ņƒ. ĐĻŅ ĐŋаĐŋĐēа, вĐēĐģŅŽŅ‡Đ°ŅŽŅ‡Đ¸ ĐŋŅ–Đ´ĐŋаĐŋĐēи, ĐąŅƒĐ´Đĩ ĐŋŅ€ĐžŅĐēаĐŊОваĐŊа ĐŊа ĐŊĐ°ŅĐ˛ĐŊŅ–ŅŅ‚ŅŒ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž.", - "library_remove_exclusion_pattern_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ҆ĐĩĐš ŅˆĐ°ĐąĐģĐžĐŊ виĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ?", + "library_folder_description": "ВĐēаĐļŅ–Ņ‚ŅŒ ĐŋаĐŋĐē҃ Đ´ĐģŅ Ņ–ĐŧĐŋĐžŅ€Ņ‚Ņƒ. ĐĻŅ ĐŋаĐŋĐēа Ņ€Đ°ĐˇĐžĐŧ Ņ–Đˇ ĐŋŅ–Đ´ĐŋаĐŋĐēаĐŧи ĐąŅƒĐ´Đĩ ĐŋŅ€ĐžŅĐēаĐŊОваĐŊа ĐŊа ĐŊĐ°ŅĐ˛ĐŊŅ–ŅŅ‚ŅŒ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž.", + "library_remove_exclusion_pattern_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ҆ĐĩĐš ŅˆĐ°ĐąĐģĐžĐŊ виĐŊŅŅ‚Đē҃?", "library_remove_folder_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ Ņ†ŅŽ ĐŋаĐŋĐē҃ Ņ–ĐŧĐŋĐžŅ€Ņ‚Ņƒ?", "library_scanning": "ПĐĩŅ€Ņ–ĐžĐ´Đ¸Ņ‡ĐŊĐĩ ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ", - "library_scanning_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€Ņ–ĐžĐ´Đ¸Ņ‡ĐŊĐĩ ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", + "library_scanning_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐĩŅ€Ņ–ĐžĐ´Đ¸Ņ‡ĐŊĐžĐŗĐž ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", "library_scanning_enable_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐŋĐĩŅ€Ņ–ĐžĐ´Đ¸Ņ‡ĐŊĐĩ ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", "library_settings": "ЗовĐŊŅ–ŅˆĐŊŅ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēа", "library_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи СОвĐŊŅ–ŅˆĐŊŅ–Ņ… ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē", - "library_tasks_description": "ĐĄĐēаĐŊŅƒĐ˛Đ°Ņ‚Đ¸ СОвĐŊŅ–ŅˆĐŊŅ– ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи ĐŊа ĐŊĐ°ŅĐ˛ĐŊŅ–ŅŅ‚ŅŒ ĐŊĐžĐ˛Đ¸Ņ… Ņ–/айО СĐŧŅ–ĐŊĐĩĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", - "library_updated": "ОĐŊОвĐģĐĩĐŊа ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēа", + "library_tasks_description": "ĐĄĐēаĐŊŅƒĐ˛Đ°Ņ‚Đ¸ СОвĐŊŅ–ŅˆĐŊŅ– ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи ĐŊа ĐŊĐ°ŅĐ˛ĐŊŅ–ŅŅ‚ŅŒ ĐŊĐžĐ˛Đ¸Ņ… Ņ–/айО СĐŧŅ–ĐŊĐĩĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", + "library_updated": "ОĐŊОвĐģĐĩĐŊĐž ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", "library_watching_enable_description": "Đ’Ņ–Đ´ŅŅ‚ĐĩĐļŅƒĐ˛Đ°Ņ‚Đ¸ СĐŧŅ–ĐŊи Ņ„Đ°ĐšĐģŅ–Đ˛ ҃ СОвĐŊŅ–ŅˆĐŊŅ–Ņ… ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēĐ°Ņ…", - "library_watching_settings": "ĐĄĐŋĐžŅŅ‚ĐĩŅ€ĐĩĐļĐĩĐŊĐŊŅ Са ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēĐžŅŽ [ЕКСПЕРИМЕНĐĸАЛĐŦНЕ]", - "library_watching_settings_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐĩ ҁĐŋĐžŅŅ‚ĐĩŅ€ĐĩĐļĐĩĐŊĐŊŅ Са СĐŧŅ–ĐŊĐĩĐŊиĐŧи Ņ„Đ°ĐšĐģаĐŧи", + "library_watching_settings": "Đ’Ņ–Đ´ŅŅ‚ĐĩĐļĐĩĐŊĐŊŅ СĐŧŅ–ĐŊ ҃ ĐąŅ–ĐąĐģŅ–ĐžŅ‚Đĩ҆Җ [ЕКСПЕРИМЕНĐĸАЛĐŦНО]", + "library_watching_settings_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐĩ Đ˛Ņ–Đ´ŅŅ‚ĐĩĐļĐĩĐŊĐŊŅ СĐŧŅ–ĐŊĐĩĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", "logging_enable_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ вĐĩĐ´ĐĩĐŊĐŊŅ ĐļŅƒŅ€ĐŊаĐģ҃", - "logging_level_description": "КоĐģи ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž, ŅĐēиК Ņ€Ņ–Đ˛ĐĩĐŊҌ ĐļŅƒŅ€ĐŊаĐģŅŽĐ˛Đ°ĐŊĐŊŅ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸.", + "logging_level_description": "Đ Ņ–Đ˛ĐĩĐŊҌ Đ´ĐĩŅ‚Đ°ĐģŅ–ĐˇĐ°Ņ†Ņ–Ņ— ĐļŅƒŅ€ĐŊаĐģ҃, ĐēĐžĐģи ĐļŅƒŅ€ĐŊаĐģŅŽĐ˛Đ°ĐŊĐŊŅ ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž.", "logging_settings": "Đ–ŅƒŅ€ĐŊаĐģŅŽĐ˛Đ°ĐŊĐŊŅ", "machine_learning_availability_checks": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēи Đ´ĐžŅŅ‚ŅƒĐŋĐŊĐžŅŅ‚Ņ–", "machine_learning_availability_checks_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž Đ˛Đ¸ŅĐ˛ĐģŅŅ‚Đ¸ Ņ‚Đ° ĐŊĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ Đ´ĐžŅŅ‚ŅƒĐŋĐŊиĐŧ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°Đŧ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ", @@ -150,71 +150,71 @@ "machine_learning_availability_checks_timeout": "ĐĸаКĐŧ-Đ°ŅƒŅ‚ СаĐŋĐ¸Ņ‚Ņƒ", "machine_learning_availability_checks_timeout_description": "ĐĸаКĐŧ-Đ°ŅƒŅ‚ ҃ ĐŧŅ–ĐģҖҁĐĩĐē҃ĐŊĐ´Đ°Ņ… Đ´ĐģŅ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēи Đ´ĐžŅŅ‚ŅƒĐŋĐŊĐžŅŅ‚Ņ–", "machine_learning_clip_model": "МодĐĩĐģҌ CLIP", - "machine_learning_clip_model_description": "ІĐŧ'Ņ ОдĐŊҖҔҗ С ĐŧОдĐĩĐģĐĩĐš CLIP, ŅĐēа ĐŋĐĩŅ€ĐĩŅ€Đ°Ņ…ĐžĐ˛Đ°ĐŊа Ņ‚ŅƒŅ‚. Đ—Đ°ŅƒĐ˛Đ°ĐļŅ‚Đĩ, Ņ‰Đž ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž СĐŊĐžĐ˛Ņƒ СаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ СавдаĐŊĐŊŅ ÂĢĐ ĐžĐˇŅƒĐŧĐŊиК ĐŋĐžŅˆŅƒĐēÂģ Đ´ĐģŅ Đ˛ŅŅ–Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ ĐŋҖҁĐģŅ СĐŧŅ–ĐŊи ĐŧОдĐĩĐģŅ–.", + "machine_learning_clip_model_description": "Назва ĐŧОдĐĩĐģŅ– CLIP ĐˇŅ– ҁĐŋĐ¸ŅĐē҃ Ņ‚ŅƒŅ‚. Đ—Đ°ŅƒĐ˛Đ°ĐļŅ‚Đĩ, Ņ‰Đž ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž виĐēĐžĐŊĐ°Ņ‚Đ¸ СавдаĐŊĐŊŅ ÂĢĐ ĐžĐˇŅƒĐŧĐŊиК ĐŋĐžŅˆŅƒĐēÂģ Đ´ĐģŅ Đ˛ŅŅ–Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ ĐŋҖҁĐģŅ СĐŧŅ–ĐŊи ĐŧОдĐĩĐģŅ–.", "machine_learning_duplicate_detection": "Đ’Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛", "machine_learning_duplicate_detection_enabled": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛", - "machine_learning_duplicate_detection_enabled_description": "Đ¯ĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž, Đ°ĐąŅĐžĐģŅŽŅ‚ĐŊĐž Ņ–Đ´ĐĩĐŊŅ‚Đ¸Ņ‡ĐŊŅ– Ņ„Đ°ĐšĐģи Đ˛ŅĐĩ ОдĐŊĐž ĐąŅƒĐ´ŅƒŅ‚ŅŒ видаĐģĐĩĐŊŅ– ҇ĐĩŅ€ĐĩС Đ´ŅƒĐąĐģŅŽĐ˛Đ°ĐŊĐŊŅ.", - "machine_learning_duplicate_detection_setting_description": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ Đ˛ĐąŅƒĐ´ĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ CLIP Đ´ĐģŅ ĐŋĐžŅˆŅƒĐē҃ ĐšĐŧĐžĐ˛Ņ–Ņ€ĐŊĐ¸Ņ… Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛", + "machine_learning_duplicate_detection_enabled_description": "Đ¯ĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž, Đ°ĐąŅĐžĐģŅŽŅ‚ĐŊĐž Ņ–Đ´ĐĩĐŊŅ‚Đ¸Ņ‡ĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ Đ˛ŅĐĩ ОдĐŊĐž ĐąŅƒĐ´Đĩ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐž ŅĐē Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸.", + "machine_learning_duplicate_detection_setting_description": "ĐŸĐžŅˆŅƒĐē ĐšĐŧĐžĐ˛Ņ–Ņ€ĐŊĐ¸Ņ… Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛ Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ CLIP-вĐĩĐēŅ‚ĐžŅ€Ņ–Đ˛", "machine_learning_enabled": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐĩ ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ", - "machine_learning_enabled_description": "Đ¯ĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž, Đ˛ŅŅ– Ņ„ŅƒĐŊĐē҆Җҗ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ ĐąŅƒĐ´ŅƒŅ‚ŅŒ виĐŧĐēĐŊĐĩĐŊŅ– ĐŊĐĩСаĐģĐĩĐļĐŊĐž Đ˛Ņ–Đ´ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ ĐŊиĐļ҇Đĩ.", + "machine_learning_enabled_description": "Đ¯ĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž, ĐļОдĐŊа С Ņ„ŅƒĐŊĐēŅ†Ņ–Đš ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ ĐŊĐĩ ĐŋŅ€Đ°Ņ†ŅŽĐ˛Đ°Ņ‚Đ¸ĐŧĐĩ, ĐŊĐĩСаĐģĐĩĐļĐŊĐž Đ˛Ņ–Đ´ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ ĐŊиĐļ҇Đĩ.", "machine_learning_facial_recognition": "РОСĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ ОйĐģĐ¸Ņ‡", - "machine_learning_facial_recognition_description": "Đ’Đ¸ŅĐ˛ĐģŅĐšŅ‚Đĩ, Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊĐ°Đ˛Đ°ĐšŅ‚Đĩ Ņ‚Đ° ĐŗŅ€ŅƒĐŋŅƒĐšŅ‚Đĩ ОйĐģĐ¸Ņ‡Ņ‡Ņ ĐŊа ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅŅ…", + "machine_learning_facial_recognition_description": "Đ’Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ, Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ Ņ‚Đ° ĐŗŅ€ŅƒĐŋŅƒĐ˛Đ°ĐŊĐŊŅ ОйĐģĐ¸Ņ‡ ĐŊа ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅŅ…", "machine_learning_facial_recognition_model": "МодĐĩĐģҌ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ ОйĐģĐ¸Ņ‡", - "machine_learning_facial_recognition_model_description": "МодĐĩĐģŅ– Đ˛Ņ–Đ´ŅĐžŅ€Ņ‚ĐžĐ˛Đ°ĐŊŅ– Са СĐŧĐĩĐŊ҈ĐĩĐŊĐŊŅĐŧ Ņ€ĐžĐˇĐŧŅ–Ņ€Ņƒ. Đ‘Ņ–ĐģŅŒŅˆŅ– ĐŧОдĐĩĐģŅ– ĐŋŅ€Đ°Ņ†ŅŽŅŽŅ‚ŅŒ ĐŋĐžĐ˛Ņ–ĐģҌĐŊŅ–ŅˆĐĩ, ĐŋĐžŅ‚Ņ€ĐĩĐąŅƒŅŽŅ‚ŅŒ ĐąŅ–ĐģҌ҈Đĩ ĐŋаĐŧ'ŅŅ‚Ņ–, аĐģĐĩ Đ´Đ°ŅŽŅ‚ŅŒ ĐēŅ€Đ°Ņ‰Ņ– Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚Đ¸. ЗвĐĩŅ€ĐŊŅ–Ņ‚ŅŒ ŅƒĐ˛Đ°ĐŗŅƒ, Ņ‰Đž ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž СĐŊĐžĐ˛Ņƒ СаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ СавдаĐŊĐŊŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡Ņ‡Ņ Đ´ĐģŅ Đ˛ŅŅ–Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ ĐŋҖҁĐģŅ СĐŧŅ–ĐŊи ĐŧОдĐĩĐģŅ–.", + "machine_learning_facial_recognition_model_description": "МодĐĩĐģŅ– Đ˛Ņ–Đ´ŅĐžŅ€Ņ‚ĐžĐ˛Đ°ĐŊŅ– Са СĐŧĐĩĐŊ҈ĐĩĐŊĐŊŅĐŧ Ņ€ĐžĐˇĐŧŅ–Ņ€Ņƒ. Đ‘Ņ–ĐģŅŒŅˆŅ– ĐŧОдĐĩĐģŅ– ĐŋŅ€Đ°Ņ†ŅŽŅŽŅ‚ŅŒ ĐŋĐžĐ˛Ņ–ĐģҌĐŊŅ–ŅˆĐĩ, ĐŋĐžŅ‚Ņ€ĐĩĐąŅƒŅŽŅ‚ŅŒ ĐąŅ–ĐģҌ҈Đĩ ĐŋаĐŧ'ŅŅ‚Ņ–, аĐģĐĩ Đ´Đ°ŅŽŅ‚ŅŒ ĐēŅ€Đ°Ņ‰Ņ– Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚Đ¸. ЗвĐĩŅ€ĐŊŅ–Ņ‚ŅŒ ŅƒĐ˛Đ°ĐŗŅƒ, Ņ‰Đž ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž виĐēĐžĐŊĐ°Ņ‚Đ¸ СавдаĐŊĐŊŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡ Đ´ĐģŅ Đ˛ŅŅ–Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ ĐŋҖҁĐģŅ СĐŧŅ–ĐŊи ĐŧОдĐĩĐģŅ–.", "machine_learning_facial_recognition_setting": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ ОйĐģĐ¸Ņ‡", - "machine_learning_facial_recognition_setting_description": "Đ¯ĐēŅ‰Đž Ņ†Ņ Ņ„ŅƒĐŊĐēŅ†Ņ–Ņ виĐŧĐēĐŊĐĩĐŊа, ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŊĐĩ ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐēĐžĐ´ŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ Đ´ĐģŅ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ ОйĐģĐ¸Ņ‡ Ņ– ĐŊĐĩ ĐąŅƒĐ´ŅƒŅ‚ŅŒ С'ŅĐ˛ĐģŅŅ‚Đ¸ŅŅ в Ņ€ĐžĐˇĐ´Ņ–ĐģŅ– \"Đ›ŅŽĐ´Đ¸\" ĐŊа ŅŅ‚ĐžŅ€Ņ–ĐŊ҆Җ \"ĐžĐŗĐģŅĐ´\".", + "machine_learning_facial_recognition_setting_description": "Đ¯ĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž, ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŊĐĩ ĐēĐžĐ´ŅƒĐ˛Đ°Ņ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ Đ´ĐģŅ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ ОйĐģĐ¸Ņ‡ Ņ– ĐŊĐĩ С'ŅĐ˛ĐģŅŅ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ в Ņ€ĐžĐˇĐ´Ņ–ĐģŅ– ÂĢĐ›ŅŽĐ´Đ¸Âģ ĐŊа ŅŅ‚ĐžŅ€Ņ–ĐŊ҆Җ ÂĢĐžĐŗĐģŅĐ´Âģ.", "machine_learning_max_detection_distance": "МаĐēŅĐ¸ĐŧаĐģҌĐŊа Đ˛Ņ–Đ´ŅŅ‚Đ°ĐŊҌ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ", - "machine_learning_max_detection_distance_description": "МаĐēŅĐ¸ĐŧаĐģҌĐŊа Đ˛Ņ–Đ´ŅŅ‚Đ°ĐŊҌ ĐŧŅ–Đļ двОĐŧа ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅĐŧи, Ņ‰ĐžĐą вОĐŊи вваĐļаĐģĐ¸ŅŅ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ°Đŧи, Đ˛Đ°Ņ€Ņ–ŅŽŅ”Ņ‚ŅŒŅŅ Đ˛Ņ–Đ´ 0.001 Đ´Đž 0.1. Đ’Đ¸Ņ‰Ņ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ дОСвОĐģŅŅŽŅ‚ŅŒ Đ˛Đ¸ŅĐ˛ĐģŅŅ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛, аĐģĐĩ ĐŧĐžĐļŅƒŅ‚ŅŒ ĐŋŅ€Đ¸ĐˇĐ˛ĐžĐ´Đ¸Ņ‚Đ¸ Đ´Đž ĐŋĐžĐŧиĐģĐēĐžĐ˛Đ¸Ņ… Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊҌ.", + "machine_learning_max_detection_distance_description": "МаĐēŅĐ¸ĐŧаĐģҌĐŊа Đ˛Ņ–Đ´ŅŅ‚Đ°ĐŊҌ ĐŧŅ–Đļ двОĐŧа ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅĐŧи, Ņ‰ĐžĐą вОĐŊи вваĐļаĐģĐ¸ŅŅ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ°Đŧи, Đ˛Đ°Ņ€Ņ–ŅŽŅ”Ņ‚ŅŒŅŅ Đ˛Ņ–Đ´ 0.001 Đ´Đž 0.1. Đ’Đ¸Ņ‰Ņ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ Đ´Đ°ŅŽŅ‚ŅŒ СĐŧĐžĐŗŅƒ Đ˛Đ¸ŅĐ˛ĐģŅŅ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛, аĐģĐĩ ĐŧĐžĐļŅƒŅ‚ŅŒ ĐŋŅ€Đ¸ĐˇĐ˛ĐžĐ´Đ¸Ņ‚Đ¸ Đ´Đž Ņ…Đ¸ĐąĐŊĐ¸Ņ… ҁĐŋŅ€Đ°Ņ†ŅŽĐ˛Đ°ĐŊҌ.", "machine_learning_max_recognition_distance": "МаĐēŅĐ¸ĐŧаĐģҌĐŊа Đ˛Ņ–Đ´ŅŅ‚Đ°ĐŊҌ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ", - "machine_learning_max_recognition_distance_description": "МаĐēŅĐ¸ĐŧаĐģҌĐŊа Đ˛Ņ–Đ´ŅŅ‚Đ°ĐŊҌ ĐŧŅ–Đļ двОĐŧа ОйĐģĐ¸Ņ‡Ņ‡ŅĐŧи, Ņ‰ĐžĐą Ņ—Ņ… вваĐļĐ°Ņ‚Đ¸ ОдĐŊŅ–Ņ”ŅŽ Ņ– Ņ‚Ņ–Ņ”ŅŽ Đļ ŅĐ°ĐŧĐžŅŽ ĐģŅŽĐ´Đ¸ĐŊĐžŅŽ, Đ˛Đ°Ņ€Ņ–ŅŽŅ”Ņ‚ŅŒŅŅ Đ˛Ņ–Đ´ 0 Đ´Đž 2. ЗĐŊиĐļĐĩĐŊĐŊŅ Ņ†ŅŒĐžĐŗĐž СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ СаĐŋĐžĐąŅ–ĐŗŅ‚Đ¸ ĐŋĐžĐŧиĐģĐēОвОĐŧ҃ виСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅŽ Đ´Đ˛ĐžŅ… Ņ€Ņ–ĐˇĐŊĐ¸Ņ… ĐģŅŽĐ´ĐĩĐš ŅĐē ОдĐŊҖҔҗ ĐžŅĐžĐąĐ¸, Ņ‚ĐžĐ´Ņ– ŅĐē ĐŋŅ–Đ´Đ˛Đ¸Ņ‰ĐĩĐŊĐŊŅ ĐšĐžĐŗĐž ĐŧĐžĐļĐĩ СаĐŋĐžĐąŅ–ĐŗŅ‚Đ¸ ĐŋĐžĐŧиĐģĐēОвОĐŧ҃ виСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅŽ ОдĐŊҖҔҗ Ņ– ҂ҖҔҗ Đļ ŅĐ°ĐŧĐžŅ— ĐģŅŽĐ´Đ¸ĐŊи ŅĐē Đ´Đ˛ĐžŅ… Ņ€Ņ–ĐˇĐŊĐ¸Ņ… ĐžŅŅ–Đą. ЗвĐĩŅ€ĐŊŅ–Ņ‚ŅŒ ŅƒĐ˛Đ°ĐŗŅƒ, Ņ‰Đž ĐģĐĩĐŗŅˆĐĩ Ой'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Đ´Đ˛ĐžŅ… ĐģŅŽĐ´ĐĩĐš, ĐŊŅ–Đļ Ņ€ĐžĐˇĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ ОдĐŊ҃ ĐģŅŽĐ´Đ¸ĐŊ҃ ĐŊа Đ´Đ˛Ņ–, Ņ‚ĐžĐŧ҃ Ņ€ĐĩĐēĐžĐŧĐĩĐŊĐ´ŅƒŅ”Ņ‚ŅŒŅŅ ĐŊĐ°Ņ…Đ¸ĐģĐ¸Ņ‚Đ¸ŅŅ ĐŊа ĐąŅ–Đē ĐŧĐĩĐŊŅˆĐžĐŗĐž ĐŋĐžŅ€ĐžĐŗŅƒ, ĐēĐžĐģи ҆Đĩ ĐŧĐžĐļĐģивО.", + "machine_learning_max_recognition_distance_description": "МаĐēŅĐ¸ĐŧаĐģҌĐŊа Đ˛Ņ–Đ´ŅŅ‚Đ°ĐŊҌ ĐŧŅ–Đļ двОĐŧа ОйĐģĐ¸Ņ‡Ņ‡ŅĐŧи, Ņ‰ĐžĐą Ņ—Ņ… вваĐļĐ°Ņ‚Đ¸ Ņ‚Ņ–Ņ”ŅŽ ŅĐ°ĐŧĐžŅŽ ĐģŅŽĐ´Đ¸ĐŊĐžŅŽ, Đ˛Đ°Ņ€Ņ–ŅŽŅ”Ņ‚ŅŒŅŅ Đ˛Ņ–Đ´ 0 Đ´Đž 2. ЗĐŊиĐļĐĩĐŊĐŊŅ Ņ†ŅŒĐžĐŗĐž СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ СаĐŋĐžĐąŅ–ĐŗŅ‚Đ¸ ĐŋĐžĐŧиĐģĐēОвОĐŧ҃ виСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅŽ Đ´Đ˛ĐžŅ… Ņ€Ņ–ĐˇĐŊĐ¸Ņ… ĐģŅŽĐ´ĐĩĐš ŅĐē ОдĐŊҖҔҗ ĐģŅŽĐ´Đ¸ĐŊи, Ņ‚ĐžĐ´Ņ– ŅĐē ĐŋŅ–Đ´Đ˛Đ¸Ņ‰ĐĩĐŊĐŊŅ ĐšĐžĐŗĐž ĐŧĐžĐļĐĩ СаĐŋĐžĐąŅ–ĐŗŅ‚Đ¸ ĐŋĐžĐŧиĐģĐēОвОĐŧ҃ виСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅŽ ҂ҖҔҗ ŅĐ°ĐŧĐžŅ— ĐģŅŽĐ´Đ¸ĐŊи ŅĐē Đ´Đ˛ĐžŅ… Ņ€Ņ–ĐˇĐŊĐ¸Ņ… ĐģŅŽĐ´ĐĩĐš. ЗвĐĩŅ€ĐŊŅ–Ņ‚ŅŒ ŅƒĐ˛Đ°ĐŗŅƒ, Ņ‰Đž ĐģĐĩĐŗŅˆĐĩ Ой'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Đ´Đ˛ĐžŅ… ĐģŅŽĐ´ĐĩĐš, ĐŊŅ–Đļ Ņ€ĐžĐˇĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ ОдĐŊ҃ ĐģŅŽĐ´Đ¸ĐŊ҃ ĐŊа Đ´Đ˛Ņ–, Ņ‚ĐžĐŧ҃ Ņ€ĐĩĐēĐžĐŧĐĩĐŊĐ´ŅƒŅ”Ņ‚ŅŒŅŅ ĐžĐąĐ¸Ņ€Đ°Ņ‚Đ¸ ĐŊиĐļŅ‡Đ¸Đš ĐŋĐžŅ€Ņ–Đŗ, ĐēĐžĐģи ҆Đĩ ĐŧĐžĐļĐģивО.", "machine_learning_min_detection_score": "ĐœŅ–ĐŊŅ–ĐŧаĐģҌĐŊиК ĐŋĐžĐēаСĐŊиĐē Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ", - "machine_learning_min_detection_score_description": "ĐœŅ–ĐŊŅ–ĐŧаĐģҌĐŊиК Ņ€Ņ–Đ˛ĐĩĐŊҌ вĐŋĐĩвĐŊĐĩĐŊĐžŅŅ‚Ņ– Đ´ĐģŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡Ņ‡Ņ Đ˛Ņ–Đ´ 0 Đ´Đž 1. НиĐļ҇Җ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ дОСвОĐģŅŅ‚ŅŒ Đ˛Đ¸ŅĐ˛ĐģŅŅ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ ОйĐģĐ¸Ņ‡, аĐģĐĩ ĐŧĐžĐļŅƒŅ‚ŅŒ ĐŋŅ€Đ¸ĐˇĐ˛ĐžĐ´Đ¸Ņ‚Đ¸ Đ´Đž ĐŋĐžĐŧиĐģĐēĐžĐ˛Đ¸Ņ… Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊҌ.", + "machine_learning_min_detection_score_description": "ĐœŅ–ĐŊŅ–ĐŧаĐģҌĐŊиК Ņ€Ņ–Đ˛ĐĩĐŊҌ Đ´ĐžŅŅ‚ĐžĐ˛Ņ–Ņ€ĐŊĐžŅŅ‚Ņ– Đ´ĐģŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡Ņ‡Ņ Đ˛Ņ–Đ´ 0 Đ´Đž 1. НиĐļ҇Җ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ дОСвОĐģŅŅ‚ŅŒ Đ˛Đ¸ŅĐ˛ĐģŅŅ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ ОйĐģĐ¸Ņ‡, аĐģĐĩ ĐŧĐžĐļŅƒŅ‚ŅŒ ĐŋŅ€Đ¸ĐˇĐ˛ĐžĐ´Đ¸Ņ‚Đ¸ Đ´Đž Ņ…Đ¸ĐąĐŊĐ¸Ņ… ҁĐŋŅ€Đ°Ņ†ŅŽĐ˛Đ°ĐŊҌ.", "machine_learning_min_recognized_faces": "ĐœŅ–ĐŊŅ–Đŧ҃Đŧ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаĐŊĐ¸Ņ… ОйĐģĐ¸Ņ‡", - "machine_learning_min_recognized_faces_description": "ĐœŅ–ĐŊŅ–ĐŧаĐģҌĐŊа ĐēŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаĐŊĐ¸Ņ… ОйĐģĐ¸Ņ‡ Đ´ĐģŅ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐžŅĐžĐąĐ¸. Đ—ĐąŅ–ĐģҌ҈ĐĩĐŊĐŊŅ Ņ†ŅŒĐžĐŗĐž ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Ņƒ Ņ€ĐžĐąĐ¸Ņ‚ŅŒ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ ОйĐģĐ¸Ņ‡ Ņ‚ĐžŅ‡ĐŊŅ–ŅˆĐ¸Đŧ, аĐģĐĩ ĐŧĐžĐļĐĩ ĐˇĐąŅ–ĐģŅŒŅˆĐ¸Ņ‚Đ¸ Ņ€Đ¸ĐˇĐ¸Đē Ņ‚ĐžĐŗĐž, Ņ‰Đž ОйĐģĐ¸Ņ‡Ņ‡Ņ ĐŊĐĩ ĐąŅƒĐ´Đĩ ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐž ĐžŅĐžĐąŅ–.", + "machine_learning_min_recognized_faces_description": "ĐœŅ–ĐŊŅ–ĐŧаĐģҌĐŊа ĐēŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаĐŊĐ¸Ņ… ОйĐģĐ¸Ņ‡ Đ´ĐģŅ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐģŅŽĐ´Đ¸ĐŊи. Đ—ĐąŅ–ĐģҌ҈ĐĩĐŊĐŊŅ Ņ†ŅŒĐžĐŗĐž ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ° Ņ€ĐžĐąĐ¸Ņ‚ŅŒ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ ОйĐģĐ¸Ņ‡ Ņ‚ĐžŅ‡ĐŊŅ–ŅˆĐ¸Đŧ, аĐģĐĩ ĐŧĐžĐļĐĩ ĐˇĐąŅ–ĐģŅŒŅˆĐ¸Ņ‚Đ¸ Ņ€Đ¸ĐˇĐ¸Đē Ņ‚ĐžĐŗĐž, Ņ‰Đž ОйĐģĐ¸Ņ‡Ņ‡Ņ ĐŊĐĩ ĐąŅƒĐ´Đĩ ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐž ĐģŅŽĐ´Đ¸ĐŊŅ–.", "machine_learning_ocr": "OCR", - "machine_learning_ocr_description": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐĩ ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ Đ´ĐģŅ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ Ņ‚ĐĩĐēŅŅ‚Ņƒ ĐŊа ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅŅ…", + "machine_learning_ocr_description": "РОСĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ Ņ‚ĐĩĐēŅŅ‚Ņƒ ĐŊа ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅŅ… Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ", "machine_learning_ocr_enabled": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ OCR", - "machine_learning_ocr_enabled_description": "Đ¯ĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž, ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŊĐĩ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊĐ°Đ˛Đ°Ņ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ Ņ‚ĐĩĐēŅŅ‚Ņƒ.", + "machine_learning_ocr_enabled_description": "Đ¯ĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž, Ņ‚ĐĩĐēҁ҂ ĐŊа ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅŅ… ĐŊĐĩ ĐąŅƒĐ´Đĩ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊĐ°Đ˛Đ°Ņ‚Đ¸ŅŅ.", "machine_learning_ocr_max_resolution": "МаĐēŅĐ¸ĐŧаĐģҌĐŊа Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊа ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ", - "machine_learning_ocr_max_resolution_description": "РОСĐŧŅ–Ņ€ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ С Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅŽ ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŽ Đ˛Đ¸Ņ‰Đĩ ҆ҖҔҗ ĐąŅƒĐ´Đĩ СĐŧŅ–ĐŊĐĩĐŊĐž ĐˇŅ– СйĐĩŅ€ĐĩĐļĐĩĐŊĐŊŅĐŧ ҁĐŋŅ–Đ˛Đ˛Ņ–Đ´ĐŊĐžŅˆĐĩĐŊĐŊŅ ŅŅ‚ĐžŅ€Ņ–ĐŊ. Đ’Đ¸Ņ‰Ņ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ Ņ‚ĐžŅ‡ĐŊŅ–ŅˆŅ–, аĐģĐĩ ĐžĐąŅ€ĐžĐąĐģŅŅŽŅ‚ŅŒŅŅ Đ´ĐžĐ˛ŅˆĐĩ Ņ‚Đ° виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅŽŅ‚ŅŒ ĐąŅ–ĐģҌ҈Đĩ ĐŋаĐŧâ€™ŅŅ‚Ņ–.", - "machine_learning_ocr_min_detection_score": "ĐœŅ–ĐŊŅ–ĐŧаĐģҌĐŊиК йаĐģ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ", - "machine_learning_ocr_min_detection_score_description": "ĐœŅ–ĐŊŅ–ĐŧаĐģҌĐŊиК йаĐģ Đ´ĐžŅŅ‚ĐžĐ˛Ņ–Ņ€ĐŊĐžŅŅ‚Ņ– Đ´ĐģŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ Ņ‚ĐĩĐēŅŅ‚Ņƒ ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ Đ˛Ņ–Đ´ 0 Đ´Đž 1. НиĐļ҇Җ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ дОСвОĐģŅŅ‚ŅŒ Đ˛Đ¸ŅĐ˛Đ¸Ņ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ Ņ‚ĐĩĐēŅŅ‚Ņƒ, аĐģĐĩ ĐŧĐžĐļŅƒŅ‚ŅŒ ĐŋŅ€Đ¸ĐˇĐ˛ĐĩŅŅ‚Đ¸ Đ´Đž Ņ…Đ¸ĐąĐŊĐžĐŋĐžĐˇĐ¸Ņ‚Đ¸Đ˛ĐŊĐ¸Ņ… Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚Ņ–Đ˛.", - "machine_learning_ocr_min_recognition_score": "ĐœŅ–ĐŊŅ–ĐŧаĐģҌĐŊиК йаĐģ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ", - "machine_learning_ocr_min_score_recognition_description": "ĐœŅ–ĐŊŅ–ĐŧаĐģҌĐŊиК йаĐģ Đ´ĐžŅŅ‚ĐžĐ˛Ņ–Ņ€ĐŊĐžŅŅ‚Ņ– Đ´ĐģŅ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐžĐŗĐž Ņ‚ĐĩĐēŅŅ‚Ņƒ ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ Đ˛Ņ–Đ´ 0 Đ´Đž 1. НиĐļ҇Җ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊĐ°ŅŽŅ‚ŅŒ ĐąŅ–ĐģҌ҈Đĩ Ņ‚ĐĩĐēŅŅ‚Ņƒ, аĐģĐĩ ĐŧĐžĐļŅƒŅ‚ŅŒ ĐŋŅ€Đ¸ĐˇĐ˛ĐĩŅŅ‚Đ¸ Đ´Đž Ņ…Đ¸ĐąĐŊĐžĐŋĐžĐˇĐ¸Ņ‚Đ¸Đ˛ĐŊĐ¸Ņ… Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚Ņ–Đ˛.", + "machine_learning_ocr_max_resolution_description": "ПоĐŋĐĩŅ€ĐĩĐ´ĐŊŅ– ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ¸ С Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅŽ ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŽ Đ˛Đ¸Ņ‰ĐžŅŽ Са вĐēаСаĐŊ҃ ĐąŅƒĐ´Đĩ СĐŧĐĩĐŊ҈ĐĩĐŊĐž ĐˇŅ– СйĐĩŅ€ĐĩĐļĐĩĐŊĐŊŅĐŧ ĐŋŅ€ĐžĐŋĐžŅ€Ņ†Ņ–Đš. Đ’Đ¸Ņ‰Ņ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ Ņ‚ĐžŅ‡ĐŊŅ–ŅˆŅ–, аĐģĐĩ ĐžĐąŅ€ĐžĐąĐģŅŅŽŅ‚ŅŒŅŅ Đ´ĐžĐ˛ŅˆĐĩ Ņ‚Đ° виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅŽŅ‚ŅŒ ĐąŅ–ĐģҌ҈Đĩ ĐŋаĐŧâ€™ŅŅ‚Ņ–.", + "machine_learning_ocr_min_detection_score": "ĐœŅ–ĐŊŅ–ĐŧаĐģҌĐŊиК ĐŋĐžĐēаСĐŊиĐē Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ", + "machine_learning_ocr_min_detection_score_description": "ĐœŅ–ĐŊŅ–ĐŧаĐģҌĐŊиК Ņ€Ņ–Đ˛ĐĩĐŊҌ Đ´ĐžŅŅ‚ĐžĐ˛Ņ–Ņ€ĐŊĐžŅŅ‚Ņ– Đ´ĐģŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ Ņ‚ĐĩĐēŅŅ‚Ņƒ Đ˛Ņ–Đ´ 0 Đ´Đž 1. НиĐļ҇Җ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ Đ´Đ°Đ˛Đ°Ņ‚Đ¸ĐŧŅƒŅ‚ŅŒ СĐŧĐžĐŗŅƒ Đ˛Đ¸ŅĐ˛Đ¸Ņ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ Ņ‚ĐĩĐēŅŅ‚Ņƒ, аĐģĐĩ ĐŧĐžĐļŅƒŅ‚ŅŒ ĐŋŅ€Đ¸ĐˇĐ˛ĐĩŅŅ‚Đ¸ Đ´Đž Ņ…Đ¸ĐąĐŊĐ¸Ņ… ҁĐŋŅ€Đ°Ņ†ŅŽĐ˛Đ°ĐŊҌ.", + "machine_learning_ocr_min_recognition_score": "ĐœŅ–ĐŊŅ–ĐŧаĐģҌĐŊиК ĐŋĐžĐēаСĐŊиĐē Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ", + "machine_learning_ocr_min_score_recognition_description": "ĐœŅ–ĐŊŅ–ĐŧаĐģҌĐŊиК Ņ€Ņ–Đ˛ĐĩĐŊҌ Đ´ĐžŅŅ‚ĐžĐ˛Ņ–Ņ€ĐŊĐžŅŅ‚Ņ– Đ´ĐģŅ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐžĐŗĐž Ņ‚ĐĩĐēŅŅ‚Ņƒ Đ˛Ņ–Đ´ 0 Đ´Đž 1. НиĐļ҇Җ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊĐ°ŅŽŅ‚ŅŒ ĐąŅ–ĐģҌ҈Đĩ Ņ‚ĐĩĐēŅŅ‚Ņƒ, аĐģĐĩ ĐŧĐžĐļŅƒŅ‚ŅŒ ĐŋŅ€Đ¸ĐˇĐ˛ĐĩŅŅ‚Đ¸ Đ´Đž Ņ…Đ¸ĐąĐŊĐ¸Ņ… ҁĐŋŅ€Đ°Ņ†ŅŽĐ˛Đ°ĐŊҌ.", "machine_learning_ocr_model": "МодĐĩĐģҌ OCR", "machine_learning_ocr_model_description": "ĐĄĐĩŅ€Đ˛ĐĩŅ€ĐŊŅ– ĐŧОдĐĩĐģŅ– Ņ‚ĐžŅ‡ĐŊŅ–ŅˆŅ– Са ĐŧĐžĐąŅ–ĐģҌĐŊŅ–, аĐģĐĩ ĐžĐąŅ€ĐžĐąĐģŅŅŽŅ‚ŅŒ даĐŊŅ– Đ´ĐžĐ˛ŅˆĐĩ Ņ‚Đ° виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅŽŅ‚ŅŒ ĐąŅ–ĐģҌ҈Đĩ ĐŋаĐŧ'ŅŅ‚Ņ–.", "machine_learning_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ", "machine_learning_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ„ŅƒĐŊĐēŅ†Ņ–ŅĐŧи Ņ‚Đ° ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ", "machine_learning_smart_search": "Đ ĐžĐˇŅƒĐŧĐŊиК ĐŋĐžŅˆŅƒĐē", - "machine_learning_smart_search_description": "ĐŸĐžŅˆŅƒĐē ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ ҁĐĩĐŧаĐŊŅ‚Đ¸Ņ‡ĐŊĐ¸Ņ… Đ˛ĐąŅƒĐ´ĐžĐ˛ŅƒĐ˛Đ°ĐŊҌ CLIP", + "machine_learning_smart_search_description": "ĐĄĐĩĐŧаĐŊŅ‚Đ¸Ņ‡ĐŊиК ĐŋĐžŅˆŅƒĐē ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ CLIP-вĐĩĐēŅ‚ĐžŅ€Ņ–Đ˛", "machine_learning_smart_search_enabled": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ€ĐžĐˇŅƒĐŧĐŊиК ĐŋĐžŅˆŅƒĐē", - "machine_learning_smart_search_enabled_description": "Đ¯ĐēŅ‰Đž Ņ†Ņ Ņ„ŅƒĐŊĐēŅ†Ņ–Ņ виĐŧĐēĐŊĐĩĐŊа, ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŊĐĩ ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐēĐžĐ´ŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ Đ´ĐģŅ Ņ€ĐžĐˇŅƒĐŧĐŊĐžĐŗĐž ĐŋĐžŅˆŅƒĐē҃.", - "machine_learning_url_description": "URL ҁĐĩŅ€Đ˛ĐĩŅ€Đ° ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ. Đ¯ĐēŅ‰Đž ĐŊадаĐŊĐž ĐąŅ–ĐģҌ҈Đĩ ОдĐŊĐžĐŗĐž URL, ҁĐĩŅ€Đ˛ĐĩŅ€Đ¸ ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžĐŋĐ¸Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ ĐŋĐž ҇ĐĩŅ€ĐˇŅ–, ĐŋĐžĐēи ОдиĐŊ С ĐŊĐ¸Ņ… ĐŊĐĩ Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–ŅŅ‚ŅŒ ҃ҁĐŋŅ–ŅˆĐŊĐž, ҃ ĐŋĐžŅ€ŅĐ´Đē҃ Đ˛Ņ–Đ´ ĐŋĐĩŅ€ŅˆĐžĐŗĐž Đ´Đž ĐžŅŅ‚Đ°ĐŊĐŊŅŒĐžĐŗĐž. ĐĄĐĩŅ€Đ˛ĐĩŅ€Đ¸, ŅĐēŅ– ĐŊĐĩ Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°ŅŽŅ‚ŅŒ, ĐąŅƒĐ´ŅƒŅ‚ŅŒ Ņ‚Đ¸ĐŧŅ‡Đ°ŅĐžĐ˛Đž Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ, ĐŋĐžĐēи ĐŊĐĩ ŅŅ‚Đ°ĐŊŅƒŅ‚ŅŒ Đ´ĐžŅŅ‚ŅƒĐŋĐŊиĐŧи.", + "machine_learning_smart_search_enabled_description": "Đ¯ĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž, ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŊĐĩ ĐēĐžĐ´ŅƒĐ˛Đ°Ņ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ Đ´ĐģŅ Ņ€ĐžĐˇŅƒĐŧĐŊĐžĐŗĐž ĐŋĐžŅˆŅƒĐē҃.", + "machine_learning_url_description": "URL ҁĐĩŅ€Đ˛ĐĩŅ€Đ° ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ. Đ¯ĐēŅ‰Đž ĐŊадаĐŊĐž ĐąŅ–ĐģҌ҈Đĩ ОдĐŊĐžĐŗĐž URL, ҁĐĩŅ€Đ˛ĐĩŅ€Đ¸ ĐžĐŋĐ¸Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ ĐŋĐž ҇ĐĩŅ€ĐˇŅ–, ĐŋĐžĐēи ОдиĐŊ С ĐŊĐ¸Ņ… ĐŊĐĩ Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–ŅŅ‚ŅŒ ҃ҁĐŋŅ–ŅˆĐŊĐž, ҃ ĐŋĐžŅ€ŅĐ´Đē҃ Đ˛Ņ–Đ´ ĐŋĐĩŅ€ŅˆĐžĐŗĐž Đ´Đž ĐžŅŅ‚Đ°ĐŊĐŊŅŒĐžĐŗĐž. ĐĄĐĩŅ€Đ˛ĐĩŅ€Đ¸, ŅĐēŅ– ĐŊĐĩ Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°ŅŽŅ‚ŅŒ, Ņ‚Đ¸ĐŧŅ‡Đ°ŅĐžĐ˛Đž Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ, ĐŋĐžĐēи ĐŊĐĩ ŅŅ‚Đ°ĐŊŅƒŅ‚ŅŒ Đ´ĐžŅŅ‚ŅƒĐŋĐŊиĐŧи.", "maintenance_delete_backup": "ВидаĐģĐ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ", "maintenance_delete_backup_description": "ĐĻĐĩĐš Ņ„Đ°ĐšĐģ ĐąŅƒĐ´Đĩ ĐąĐĩСĐŋĐžĐ˛ĐžŅ€ĐžŅ‚ĐŊĐž видаĐģĐĩĐŊĐž.", "maintenance_delete_error": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ.", - "maintenance_restore_backup": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ—", + "maintenance_restore_backup": "Đ’Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ", "maintenance_restore_backup_description": "Immich ĐąŅƒĐ´Đĩ ҁ҂ĐĩŅ€Ņ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž С Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžŅ— Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ—. ПĐĩŅ€ĐĩĐ´ ĐŋŅ€ĐžĐ´ĐžĐ˛ĐļĐĩĐŊĐŊŅĐŧ ĐąŅƒĐ´Đĩ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ.", "maintenance_restore_backup_different_version": "ĐĻŅŽ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ ĐąŅƒĐģĐž ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ Ņ–ĐŊŅˆĐžŅ— вĐĩҀҁҖҗ Immich!", "maintenance_restore_backup_unknown_version": "НĐĩ вдаĐģĐžŅŅ виСĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ вĐĩŅ€ŅŅ–ŅŽ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ—.", - "maintenance_restore_database_backup": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ— йаСи даĐŊĐ¸Ņ…", - "maintenance_restore_database_backup_description": "Đ’Ņ–Đ´ĐēĐ°Ņ‚ Đ´Đž ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ŅŅ‚Đ°ĐŊ҃ йаСи даĐŊĐ¸Ņ… Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ Ņ„Đ°ĐšĐģ҃ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ—", - "maintenance_settings": "ĐĸĐĩŅ…ĐŊҖ҇ĐŊĐĩ ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ", - "maintenance_settings_description": "ПĐĩŅ€ĐĩвĐĩĐ´ĐĩĐŊĐŊŅ Immich ҃ Ņ€ĐĩĐļиĐŧ Ņ‚ĐĩŅ…ĐŊҖ҇ĐŊĐžĐŗĐž ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ", - "maintenance_start": "ПĐĩŅ€ĐĩŅ…Ņ–Đ´ ҃ Ņ€ĐĩĐļиĐŧ Ņ‚ĐĩŅ…ĐŊҖ҇ĐŊĐžĐŗĐž ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ", - "maintenance_start_error": "НĐĩ вдаĐģĐžŅŅ СаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ.", + "maintenance_restore_database_backup": "Đ’Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ йаСи даĐŊĐ¸Ņ…", + "maintenance_restore_database_backup_description": "ПовĐĩŅ€ĐŊĐĩĐŊĐŊŅ Đ´Đž ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ŅŅ‚Đ°ĐŊ҃ йаСи даĐŊĐ¸Ņ… Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ Ņ„Đ°ĐšĐģ҃ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ—", + "maintenance_settings": "ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ", + "maintenance_settings_description": "ПĐĩŅ€ĐĩвĐĩĐ´ĐĩĐŊĐŊŅ Immich ҃ Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ", + "maintenance_start": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ в Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ", + "maintenance_start_error": "НĐĩ вдаĐģĐžŅŅ ŅƒĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ.", "maintenance_upload_backup": "ВиваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ— йаСи даĐŊĐ¸Ņ…", "maintenance_upload_backup_error": "НĐĩ вдаĐģĐžŅŅ виваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ, ҆Đĩ Ņ„Đ°ĐšĐģ .sql/.sql.gz?", "manage_concurrency": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊŅ–ŅŅ‚ŅŽ СавдаĐŊҌ", - "manage_concurrency_description": "ПĐĩŅ€ĐĩŅ…Ņ–Đ´ Đ´Đž ŅŅ‚ĐžŅ€Ņ–ĐŊĐēи СавдаĐŊҌ Đ´ĐģŅ ĐēĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊŅ–ŅŅ‚ŅŽ", + "manage_concurrency_description": "ПĐĩŅ€ĐĩĐšĐ´Ņ–Ņ‚ŅŒ Đ´Đž ŅŅ‚ĐžŅ€Ņ–ĐŊĐēи СавдаĐŊҌ, Ņ‰ĐžĐą ĐēĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊŅ–ŅŅ‚ŅŽ", "manage_log_settings": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐļŅƒŅ€ĐŊаĐģ҃", "map_dark_style": "ĐĸĐĩĐŧĐŊиК ŅŅ‚Đ¸ĐģҌ", "map_enable_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ„ŅƒĐŊĐē҆Җҗ ĐŧаĐŋи", - "map_gps_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧаĐŋи Ņ‚Đ° ĐŗĐĩĐžĐģĐžĐēĐ°Ņ†Ņ–Ņ—", - "map_gps_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐŧаĐŋи Ņ‚Đ° ĐŗĐĩĐžĐģĐžĐēĐ°Ņ†Ņ–Ņ— (ĐˇĐ˛ĐžŅ€ĐžŅ‚ĐŊиК ĐŗĐĩĐžĐēОдиĐŊĐŗ)", - "map_implications": "Đ¤ŅƒĐŊĐēŅ†Ņ–Ņ ĐŧаĐŋи виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ” СОвĐŊŅ–ŅˆĐŊŅ–Đš ҁĐĩŅ€Đ˛Ņ–Ņ ĐŋĐģĐ¸Ņ‚ĐžĐē (tiles.immich.cloud)", + "map_gps_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧаĐŋи Ņ‚Đ° GPS", + "map_gps_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐŧаĐŋи Ņ‚Đ° GPS (ĐˇĐ˛ĐžŅ€ĐžŅ‚ĐŊĐĩ ĐŗĐĩĐžĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ)", + "map_implications": "Đ¤ŅƒĐŊĐēŅ†Ņ–Ņ ĐŧаĐŋи виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ” СОвĐŊŅ–ŅˆĐŊŅŽ ҁĐģ҃ĐļĐąŅƒ Ņ‚Đ°ĐšĐģŅ–Đ˛ (tiles.immich.cloud)", "map_light_style": "ĐĄĐ˛Ņ–Ņ‚ĐģиК ŅŅ‚Đ¸ĐģҌ", - "map_manage_reverse_geocoding_settings": "КĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐˇĐ˛ĐžŅ€ĐžŅ‚ĐŊĐžĐŗĐž ĐŗĐĩĐžĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ", + "map_manage_reverse_geocoding_settings": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐˇĐ˛ĐžŅ€ĐžŅ‚ĐŊĐžĐŗĐž ĐŗĐĩĐžĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ", "map_reverse_geocoding": "Đ—Đ˛ĐžŅ€ĐžŅ‚ĐŊĐĩ ĐŗĐĩĐžĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ", "map_reverse_geocoding_enable_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐˇĐ˛ĐžŅ€ĐžŅ‚ĐŊĐĩ ĐŗĐĩĐžĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ", "map_reverse_geocoding_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐˇĐ˛ĐžŅ€ĐžŅ‚ĐŊĐžĐŗĐž ĐŗĐĩĐžĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ", @@ -222,23 +222,23 @@ "map_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐŧаĐŋи", "map_style_description": "URL Đ´Đž Ņ‚ĐĩĐŧи ĐŧаĐŋи ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ– style.json", "memory_cleanup_job": "ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐŊŅ ҁĐŋĐžĐŗĐ°Đ´Ņ–Đ˛", - "memory_generate_job": "ГĐĩĐŊĐĩŅ€Đ°Ņ†Ņ–Ņ ҁĐŋĐžĐŗĐ°Đ´Ņ–Đ˛", - "metadata_extraction_job": "Đ’Đ¸Ņ‚ŅĐŗĐŊŅƒŅ‚Đ¸ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊŅ–", - "metadata_extraction_job_description": "Đ’Đ¸Đ´ĐžĐąŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ…: ĐŗĐĩОдаĐŊŅ–, Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаĐŊŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ Ņ‚Đ° Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊа ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ", + "memory_generate_job": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ҁĐŋĐžĐŗĐ°Đ´Ņ–Đ˛", + "metadata_extraction_job": "Đ’Đ¸Đ´ĐžĐąŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ…", + "metadata_extraction_job_description": "Đ’Đ¸Đ´ĐžĐąŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ… С ĐēĐžĐļĐŊĐžĐŗĐž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°, СОĐēŅ€ĐĩĐŧа: GPS-ĐēĐžĐžŅ€Đ´Đ¸ĐŊĐ°Ņ‚Đ¸, ОйĐģĐ¸Ņ‡Ņ‡Ņ Ņ‚Đ° Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊа ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ", "metadata_faces_import_setting": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ–ĐŧĐŋĐžŅ€Ņ‚ ОйĐģĐ¸Ņ‡", - "metadata_faces_import_setting_description": "ІĐŧĐŋĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ ОйĐģĐ¸Ņ‡Ņ‡Ņ С EXIF-даĐŊĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ Ņ‚Đ° sidecar-Ņ„Đ°ĐšĐģŅ–Đ˛", + "metadata_faces_import_setting_description": "ІĐŧĐŋĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ ОйĐģĐ¸Ņ‡Ņ‡Ņ С Exif-даĐŊĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ Ņ‚Đ° sidecar-Ņ„Đ°ĐšĐģŅ–Đ˛", "metadata_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ…", "metadata_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ…", "migration_job": "ĐœŅ–ĐŗŅ€Đ°Ņ†Ņ–Ņ", - "migration_job_description": "ПĐĩŅ€ĐĩĐŊĐĩҁĐĩĐŊĐŊŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€ Ņ„Đ°ĐšĐģŅ–Đ˛ Ņ‚Đ° ОйĐģĐ¸Ņ‡ŅŒ Đ´Đž ĐžĐŊОвĐģĐĩĐŊĐžŅ— ŅŅ‚Ņ€ŅƒĐēŅ‚ŅƒŅ€Đ¸ ĐŋаĐŋĐžĐē", - "nightly_tasks_cluster_faces_setting_description": "ЗаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ ОйĐģĐ¸Ņ‡ ĐŊа Ņ‰ĐžĐšĐŊĐž Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐ¸Ņ… ОйĐģĐ¸Ņ‡Ņ‡ŅŅ…", + "migration_job_description": "ПĐĩŅ€ĐĩĐŊĐĩҁĐĩĐŊĐŊŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ Ņ‚Đ° ОйĐģĐ¸Ņ‡ Đ´Đž ĐžĐŊОвĐģĐĩĐŊĐžŅ— ŅŅ‚Ņ€ŅƒĐēŅ‚ŅƒŅ€Đ¸ ĐŋаĐŋĐžĐē", + "nightly_tasks_cluster_faces_setting_description": "ВиĐēĐžĐŊаĐŊĐŊŅ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ ОйĐģĐ¸Ņ‡ Đ´ĐģŅ ĐŊĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊĐž Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐ¸Ņ… ОйĐģĐ¸Ņ‡", "nightly_tasks_cluster_new_faces_setting": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– ОйĐģĐ¸Ņ‡Ņ‡Ņ", "nightly_tasks_database_cleanup_setting": "ЗавдаĐŊĐŊŅ С ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐŊŅ йаСи даĐŊĐ¸Ņ…", "nightly_tasks_database_cleanup_setting_description": "ВидаĐģĐĩĐŊĐŊŅ ŅŅ‚Đ°Ņ€Đ¸Ņ… Ņ– ĐŋŅ€ĐžŅŅ‚Ņ€ĐžŅ‡ĐĩĐŊĐ¸Ņ… даĐŊĐ¸Ņ… Ņ–Đˇ йаСи даĐŊĐ¸Ņ…", - "nightly_tasks_generate_memories_setting": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ҁĐŋĐžĐŗĐ°Đ´Đ¸", - "nightly_tasks_generate_memories_setting_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž ŅŅ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– ҁĐŋĐžĐŗĐ°Đ´Đ¸ С ĐŊĐ°ŅĐ˛ĐŊĐ¸Ņ… Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž Ņ‰ĐžĐŊĐžŅ‡Ņ–", + "nightly_tasks_generate_memories_setting": "ĐĄŅ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ҁĐŋĐžĐŗĐ°Đ´Đ¸", + "nightly_tasks_generate_memories_setting_description": "ĐĄŅ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– ҁĐŋĐžĐŗĐ°Đ´Đ¸ С ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", "nightly_tasks_missing_thumbnails_setting": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ– ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸", - "nightly_tasks_missing_thumbnails_setting_description": "ЧĐĩŅ€ĐŗĐ° Đ´ĐģŅ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€ Đ´ĐģŅ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ĐąĐĩС ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€", + "nightly_tasks_missing_thumbnails_setting_description": "ĐŸĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚Đ¸ в ҇ĐĩŅ€ĐŗŅƒ ĐŊа ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸, ŅĐēŅ– Ņ—Ņ… ĐŊĐĩ ĐŧĐ°ŅŽŅ‚ŅŒ", "nightly_tasks_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊҖ҇ĐŊĐ¸Ņ… СавдаĐŊҌ", "nightly_tasks_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊҖ҇ĐŊиĐŧи СавдаĐŊĐŊŅĐŧи", "nightly_tasks_start_time_setting": "Đ§Đ°Ņ ĐŋĐžŅ‡Đ°Ņ‚Đē҃", @@ -247,33 +247,33 @@ "nightly_tasks_sync_quota_usage_setting_description": "ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐēĐ˛ĐžŅ‚Ņƒ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ° ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐŊа ĐžŅĐŊĐžĐ˛Ņ– ĐŋĐžŅ‚ĐžŅ‡ĐŊĐžĐŗĐž виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ", "no_paths_added": "ШĐģŅŅ…Đ¸ ĐŊĐĩ дОдаĐŊĐž", "no_pattern_added": "ШайĐģĐžĐŊ ĐŊĐĩ дОдаĐŊĐž", - "note_apply_storage_label_previous_assets": "ĐŸŅ€Đ¸ĐŧŅ–Ņ‚Đēа: ЊОй ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧŅ–Ņ‚Đē҃ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ Đ´Đž Ņ€Đ°ĐŊŅ–ŅˆĐĩ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛, СаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸", + "note_apply_storage_label_previous_assets": "ĐŸŅ€Đ¸ĐŧŅ–Ņ‚Đēа: ЊОй ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧŅ–Ņ‚Đē҃ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ Đ´Đž Ņ€Đ°ĐŊŅ–ŅˆĐĩ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛, виĐēĐžĐŊĐ°ĐšŅ‚Đĩ", "note_cannot_be_changed_later": "ПРИМІĐĸКА: ĐĻĐĩ ĐŊĐĩ ĐŧĐžĐļĐŊа СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋŅ–ĐˇĐŊŅ–ŅˆĐĩ!", - "notification_email_from_address": "ĐĐ´Ņ€ĐĩŅĐ° ĐŊĐ°Đ´ŅĐ¸ĐģĐ°Ņ‡Đ°", - "notification_email_from_address_description": "ĐĐ´Ņ€ĐĩŅĐ° ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅ— ĐŋĐžŅˆŅ‚Đ¸ ĐŊĐ°Đ´ŅĐ¸ĐģĐ°Ņ‡Đ°, ĐŊаĐŋŅ€Đ¸ĐēĐģад: \"Immich Photo Server \". ПĐĩŅ€ĐĩĐēĐžĐŊĐ°ĐšŅ‚ĐĩŅŅ, Ņ‰Đž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ Đ°Đ´Ņ€Đĩҁ҃, С ŅĐēĐžŅ— ваĐŧ дОСвОĐģĐĩĐŊĐž ĐŊĐ°Đ´ŅĐ¸ĐģĐ°Ņ‚Đ¸ ĐģĐ¸ŅŅ‚Đ¸.", + "notification_email_from_address": "ĐĐ´Ņ€ĐĩŅĐ° Đ˛Ņ–Đ´ĐŋŅ€Đ°Đ˛ĐŊиĐēа", + "notification_email_from_address_description": "ĐĐ´Ņ€ĐĩŅĐ° ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅ— ĐŋĐžŅˆŅ‚Đ¸ Đ˛Ņ–Đ´ĐŋŅ€Đ°Đ˛ĐŊиĐēа, ĐŊаĐŋŅ€Đ¸ĐēĐģад: \"Immich Photo Server \". ПĐĩŅ€ĐĩĐēĐžĐŊĐ°ĐšŅ‚ĐĩŅŅ, Ņ‰Đž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ Đ°Đ´Ņ€Đĩҁ҃, С ŅĐēĐžŅ— ваĐŧ дОСвОĐģĐĩĐŊĐž ĐŊĐ°Đ´ŅĐ¸ĐģĐ°Ņ‚Đ¸ ĐģĐ¸ŅŅ‚Đ¸.", "notification_email_host_description": "ĐĐ´Ņ€ĐĩŅĐ° ĐŋĐžŅˆŅ‚ĐžĐ˛ĐžĐŗĐž ҁĐĩŅ€Đ˛ĐĩŅ€Đ° (ĐŊаĐŋŅ€Đ¸ĐēĐģад, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Đ†ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžĐŧиĐģĐēи ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ°", "notification_email_ignore_certificate_errors_description": "Đ†ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžĐŧиĐģĐēи ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēи ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Ņ–Đ˛ TLS (ĐŊĐĩ Ņ€ĐĩĐēĐžĐŧĐĩĐŊĐ´ŅƒŅ”Ņ‚ŅŒŅŅ)", - "notification_email_password_description": "ĐŸĐ°Ņ€ĐžĐģҌ Đ´ĐģŅ Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ— ĐŊа ĐŋĐžŅˆŅ‚ĐžĐ˛ĐžĐŧ҃ ҁĐĩŅ€Đ˛ĐĩҀҖ", + "notification_email_password_description": "ĐŸĐ°Ņ€ĐžĐģҌ Đ´ĐģŅ Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ— ĐŊа ĐŋĐžŅˆŅ‚ĐžĐ˛ĐžĐŧ҃ ҁĐĩŅ€Đ˛ĐĩҀҖ", "notification_email_port_description": "ĐŸĐžŅ€Ņ‚ ĐŋĐžŅˆŅ‚ĐžĐ˛ĐžĐŗĐž ҁĐĩŅ€Đ˛ĐĩŅ€Đ° (ĐŊаĐŋŅ€Đ¸ĐēĐģад, 25, 465 айО 587)", "notification_email_secure": "SMTPS", "notification_email_secure_description": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ SMTPS (SMTP ҇ĐĩŅ€ĐĩС TLS)", "notification_email_sent_test_email_button": "ĐĐ°Đ´Ņ–ŅĐģĐ°Ņ‚Đ¸ Ņ‚ĐĩŅŅ‚ĐžĐ˛Đ¸Đš ĐģĐ¸ŅŅ‚ Ņ– СйĐĩŅ€ĐĩĐŗŅ‚Đ¸", - "notification_email_setting_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Đ´ĐģŅ ĐŊĐ°Đ´ŅĐ¸ĐģаĐŊĐŊŅ email-ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊҌ", + "notification_email_setting_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊĐ°Đ´ŅĐ¸ĐģаĐŊĐŊŅ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊҌ ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅŽ ĐŋĐžŅˆŅ‚ĐžŅŽ", "notification_email_test_email": "ĐĐ°Đ´Ņ–ŅĐģĐ°Ņ‚Đ¸ Ņ‚ĐĩŅŅ‚ĐžĐ˛Đ¸Đš ĐģĐ¸ŅŅ‚", - "notification_email_test_email_failed": "НĐĩ вдаĐģĐžŅŅ ĐŊĐ°Đ´Ņ–ŅĐģĐ°Ņ‚Đ¸ Ņ‚ĐĩŅŅ‚ĐžĐ˛Đ¸Đš ĐģĐ¸ŅŅ‚. ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ Đ˛Đ°ŅˆŅ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ", - "notification_email_test_email_sent": "ĐĸĐĩŅŅ‚ĐžĐ˛Đ¸Đš ĐģĐ¸ŅŅ‚ ĐąŅƒĐģĐž ĐŊĐ°Đ´Ņ–ŅĐģаĐŊĐž ĐŊа {email}. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ ŅĐ˛ĐžŅŽ ҁĐēŅ€Đ¸ĐŊҌĐē҃ Đ˛Ņ…Ņ–Đ´ĐŊĐ¸Ņ….", + "notification_email_test_email_failed": "НĐĩ вдаĐģĐžŅŅ ĐŊĐ°Đ´Ņ–ŅĐģĐ°Ņ‚Đ¸ Ņ‚ĐĩŅŅ‚ĐžĐ˛Đ¸Đš ĐģĐ¸ŅŅ‚. ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ ввĐĩĐ´ĐĩĐŊŅ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ", + "notification_email_test_email_sent": "ĐĸĐĩŅŅ‚ĐžĐ˛Đ¸Đš ĐģĐ¸ŅŅ‚ ĐąŅƒĐģĐž ĐŊĐ°Đ´Ņ–ŅĐģаĐŊĐž ĐŊа {email}. ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ Đ˛Ņ…Ņ–Đ´ĐŊ҃ ĐŋĐžŅˆŅ‚Ņƒ.", "notification_email_username_description": "ІĐŧ'Ņ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Đ´ĐģŅ Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ— ĐŊа ĐŋĐžŅˆŅ‚ĐžĐ˛ĐžĐŧ҃ ҁĐĩŅ€Đ˛ĐĩҀҖ", "notification_enable_email_notifications": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅŽ ĐŋĐžŅˆŅ‚ĐžŅŽ", "notification_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊҌ", - "notification_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊҌ, вĐēĐģŅŽŅ‡ĐŊĐž Ņ–Đˇ ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅŽ ĐŋĐžŅˆŅ‚ĐžŅŽ", + "notification_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊҌ, вĐēĐģŅŽŅ‡ĐŊĐž С ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅŽ ĐŋĐžŅˆŅ‚ĐžŅŽ", "oauth_auto_launch": "ĐĐ˛Ņ‚ĐžĐˇĐ°Đŋ҃ҁĐē", - "oauth_auto_launch_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž СаĐŋ҃ҁĐēĐ°Ņ‚Đ¸ ĐŋŅ€ĐžŅ†Đĩҁ Đ˛Ņ…ĐžĐ´Ņƒ ҇ĐĩŅ€ĐĩС OAuth ĐŋŅ€Đ¸ ĐŋĐĩŅ€ĐĩŅ…ĐžĐ´Ņ– ĐŊа ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃ Đ˛Ņ…ĐžĐ´Ņƒ", + "oauth_auto_launch_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž Ņ€ĐžĐˇĐŋĐžŅ‡Đ¸ĐŊĐ°Ņ‚Đ¸ Đ˛Ņ…Ņ–Đ´ ҇ĐĩŅ€ĐĩС OAuth ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŋĐĩŅ€ĐĩŅ…ĐžĐ´Ņƒ ĐŊа ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃ Đ˛Ņ…ĐžĐ´Ņƒ", "oauth_auto_register": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊа Ņ€ĐĩŅ”ŅŅ‚Ņ€Đ°Ņ†Ņ–Ņ", "oauth_auto_register_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž Ņ€ĐĩŅ”ŅŅ‚Ņ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐžĐ˛Đ¸Ņ… ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ ĐŋҖҁĐģŅ Đ˛Ņ…ĐžĐ´Ņƒ ҇ĐĩŅ€ĐĩС OAuth", "oauth_button_text": "ĐĸĐĩĐēҁ҂ ĐēĐŊĐžĐŋĐēи", "oauth_client_secret_description": "Обов'ŅĐˇĐēОвО Đ´ĐģŅ ĐēĐžĐŊŅ„Ņ–Đ´ĐĩĐŊŅ†Ņ–ĐšĐŊĐžĐŗĐž ĐēĐģŅ–Ņ”ĐŊŅ‚Đ° айО ŅĐēŅ‰Đž PKCE (ĐēĐģŅŽŅ‡ ĐŋŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´ĐļĐĩĐŊĐŊŅ Đ´ĐģŅ ОйĐŧŅ–ĐŊ҃ ĐēОдОĐŧ) ĐŊĐĩ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ”Ņ‚ŅŒŅŅ Đ´ĐģŅ ĐŋŅƒĐąĐģҖ҇ĐŊĐžĐŗĐž ĐēĐģŅ–Ņ”ĐŊŅ‚Đ°.", - "oauth_enable_description": "ĐŖĐ˛Ņ–ĐšŅ‚Đ¸ Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ OAuth", + "oauth_enable_description": "Đ’Ņ…Ņ–Đ´ Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ OAuth", "oauth_mobile_redirect_uri": "URI ĐŧĐžĐąŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŊаĐŋŅ€Đ°Đ˛ĐģĐĩĐŊĐŊŅ", "oauth_mobile_redirect_uri_override": "ПĐĩŅ€ĐĩвиСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ URI ĐŧĐžĐąŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŊаĐŋŅ€Đ°Đ˛ĐģĐĩĐŊĐŊŅ", "oauth_mobile_redirect_uri_override_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸, ŅĐēŅ‰Đž OAuth-ĐŋŅ€ĐžĐ˛Đ°ĐšĐ´ĐĩŅ€ ĐŊĐĩ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ” ĐŧĐžĐąŅ–ĐģҌĐŊиК URI, ŅĐē ''{callback}''", @@ -281,31 +281,31 @@ "oauth_role_claim_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž ĐŊĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋŅ€Đ°Đ˛Đ° адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ° ĐŊа ĐžŅĐŊĐžĐ˛Ņ– ĐŊĐ°ŅĐ˛ĐŊĐžŅŅ‚Ņ– Ņ†ŅŒĐžĐŗĐž Đ°Ņ‚Ņ€Đ¸ĐąŅƒŅ‚Ņƒ. ĐĻĐĩĐš Đ°Ņ‚Ņ€Đ¸ĐąŅƒŅ‚ ĐŧĐžĐļĐĩ ĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ‘user’ айО ‘admin’.", "oauth_settings": "OAuth", "oauth_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи Đ˛Ņ…ĐžĐ´Ņƒ ҇ĐĩŅ€ĐĩС OAuth", - "oauth_settings_more_details": "ДĐģŅ ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛ĐžŅ— Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ— ĐŋŅ€Đž Ņ†ŅŽ Ņ„ŅƒĐŊĐēŅ†Ņ–ŅŽ, СвĐĩŅ€ĐŊŅ–Ņ‚ŅŒŅŅ Đ´Đž Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ–Ņ—.", - "oauth_storage_label_claim": "ĐĸĐĩĐŗ ĐŋаĐŋĐēи ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", - "oauth_storage_label_claim_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž Đ˛ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐŧŅ–Ņ‚Đē҃ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐŊа СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ҆ҖҔҗ виĐŧĐžĐŗĐ¸.", - "oauth_storage_quota_claim": "Đ—Đ°ŅĐ˛Đēа ĐŊа ĐēĐ˛ĐžŅ‚Ņƒ ĐŊа СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ", - "oauth_storage_quota_claim_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž Đ˛ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐēĐ˛ĐžŅ‚Ņƒ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ° ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐŊа СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ҆ҖҔҗ виĐŧĐžĐŗĐ¸.", - "oauth_storage_quota_default": "ĐšĐ˛ĐžŅ‚Đ° Са СаĐŧĐžĐ˛Ņ‡ŅƒĐ˛Đ°ĐŊĐŊŅĐŧ (GiB)", - "oauth_storage_quota_default_description": "ĐšĐ˛ĐžŅ‚Đ° в GiB, Ņ‰Đž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ, ĐēĐžĐģи ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊĐĩ ĐŊадаĐŊĐž.", + "oauth_settings_more_details": "ЊОй Đ´Ņ–ĐˇĐŊĐ°Ņ‚Đ¸ŅŅ ĐąŅ–ĐģҌ҈Đĩ ĐŋŅ€Đž Ņ†ŅŽ Ņ„ŅƒĐŊĐēŅ†Ņ–ŅŽ, СвĐĩŅ€ĐŊŅ–Ņ‚ŅŒŅŅ Đ´Đž Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ–Ņ—.", + "oauth_storage_label_claim": "ĐŅ‚Ņ€Đ¸ĐąŅƒŅ‚ ĐŧŅ–Ņ‚Đēи СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ", + "oauth_storage_label_claim_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž ŅƒŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐŧŅ–Ņ‚Đē҃ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐŊа СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ Ņ†ŅŒĐžĐŗĐž Đ°Ņ‚Ņ€Đ¸ĐąŅƒŅ‚Ņƒ.", + "oauth_storage_quota_claim": "ĐŅ‚Ņ€Đ¸ĐąŅƒŅ‚ ĐēĐ˛ĐžŅ‚Đ¸ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", + "oauth_storage_quota_claim_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž ŅƒŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐēĐ˛ĐžŅ‚Ņƒ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ° ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐŊа СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ Ņ†ŅŒĐžĐŗĐž Đ°Ņ‚Ņ€Đ¸ĐąŅƒŅ‚Ņƒ.", + "oauth_storage_quota_default": "ĐĸиĐŋОва ĐēĐ˛ĐžŅ‚Đ° ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ° (GiB)", + "oauth_storage_quota_default_description": "ĐšĐ˛ĐžŅ‚Đ° в GiB, Ņ‰Đž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ, ĐēĐžĐģи Đ°Ņ‚Ņ€Đ¸ĐąŅƒŅ‚ ĐŊĐĩ ĐŊадаĐŊĐž.", "oauth_timeout": "ĐĸаКĐŧ-Đ°ŅƒŅ‚ Đ´ĐģŅ СаĐŋĐ¸Ņ‚Ņ–Đ˛", "oauth_timeout_description": "МаĐēŅĐ¸ĐŧаĐģҌĐŊиК Ņ‡Đ°Ņ ĐžŅ‡Ņ–ĐēŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Ņ– в ĐŧŅ–ĐģҖҁĐĩĐē҃ĐŊĐ´Đ°Ņ…", - "ocr_job_description": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐĩ ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ Đ´ĐģŅ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ Ņ‚ĐĩĐēŅŅ‚Ņƒ ĐŊа ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅŅ…", - "password_enable_description": "ĐŖĐ˛Ņ–ĐšŅ‚Đ¸ Са ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅŽ ĐŋĐžŅˆŅ‚ĐžŅŽ Ņ‚Đ° ĐŋĐ°Ņ€ĐžĐģĐĩĐŧ", - "password_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛Ņ…ĐžĐ´Ņƒ С ĐŋĐ°Ņ€ĐžĐģĐĩĐŧ", + "ocr_job_description": "РОСĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ Ņ‚ĐĩĐēŅŅ‚Ņƒ ĐŊа ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅŅ… Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ", + "password_enable_description": "Đ’Ņ…Ņ–Đ´ Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅ— ĐŋĐžŅˆŅ‚Đ¸ Ņ‚Đ° ĐŋĐ°Ņ€ĐžĐģŅ", + "password_settings": "Đ’Ņ…Ņ–Đ´ Са ĐŋĐ°Ņ€ĐžĐģĐĩĐŧ", "password_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи Đ˛Ņ…ĐžĐ´Ņƒ Са ĐŋĐ°Ņ€ĐžĐģĐĩĐŧ", - "paths_validated_successfully": "ĐŖŅŅ– ҈ĐģŅŅ…Đ¸ ҃ҁĐŋŅ–ŅˆĐŊĐž ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€ĐĩĐŊĐž", - "person_cleanup_job": "ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐŊŅ ĐžŅĐžĐąĐ¸", + "paths_validated_successfully": "ĐŖŅŅ– ҈ĐģŅŅ…Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€ĐĩĐŊĐž", + "person_cleanup_job": "ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐŊŅ даĐŊĐ¸Ņ… ĐģŅŽĐ´ĐĩĐš", "queue_details": "ДĐĩŅ‚Đ°ĐģŅ– ҇ĐĩŅ€ĐŗĐ¸", "queues": "ЧĐĩŅ€ĐŗĐ¸ СавдаĐŊҌ", "queues_page_description": "ĐĄŅ‚ĐžŅ€Ņ–ĐŊĐēа ҇ĐĩŅ€Đŗ СавдаĐŊҌ адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", "quota_size_gib": "РОСĐŧŅ–Ņ€ ĐēĐ˛ĐžŅ‚Đ¸ (GiB)", "refreshing_all_libraries": "ОĐŊОвĐģĐĩĐŊĐŊŅ Đ˛ŅŅ–Ņ… ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē", "registration": "Đ ĐĩŅ”ŅŅ‚Ņ€Đ°Ņ†Ņ–Ņ адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", - "registration_description": "ĐžŅĐēŅ–ĐģҌĐēи ви ĐŋĐĩŅ€ŅˆĐ¸Đš ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ в ŅĐ¸ŅŅ‚ĐĩĐŧŅ–, ви ĐąŅƒĐ´ĐĩŅ‚Đĩ ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊŅ– АдĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€ĐžĐŧ Ņ– Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°Ņ‚Đ¸ĐŧĐĩŅ‚Đĩ Са адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚Đ¸Đ˛ĐŊŅ– СавдаĐŊĐŊŅ, а Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Ņ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ– ĐąŅƒĐ´ŅƒŅ‚ŅŒ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊŅ– ваĐŧи.", + "registration_description": "ĐžŅĐēŅ–ĐģҌĐēи ви ĐŋĐĩŅ€ŅˆĐ¸Đš ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ ҃ ŅĐ¸ŅŅ‚ĐĩĐŧŅ–, Đ˛Đ°Ņ ĐąŅƒĐ´Đĩ ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐž адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€ĐžĐŧ Ņ– ви Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°Ņ‚Đ¸ĐŧĐĩŅ‚Đĩ Са адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚Đ¸Đ˛ĐŊŅ– СавдаĐŊĐŊŅ, а Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Đ¸Ņ… ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ ŅŅ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ĐŧĐĩŅ‚Đĩ ви.", "remove_failed_jobs": "ВиĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ĐŊĐĩвдаĐģŅ– СавдаĐŊĐŊŅ", - "require_password_change_on_login": "ВиĐŧĐ°ĐŗĐ°Ņ‚Đ¸ СĐŧŅ–ĐŊи ĐŋĐ°Ņ€ĐžĐģŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐŋŅ€Đ¸ ĐŋĐĩŅ€ŅˆĐžĐŧ҃ Đ˛Ņ…ĐžĐ´Ņ–", - "reset_settings_to_default": "ĐĄĐēиĐŊŅƒŅ‚Đ¸ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Đ´Đž ĐŋĐžŅ‡Đ°Ņ‚ĐēĐžĐ˛Đ¸Ņ… СĐŊĐ°Ņ‡ĐĩĐŊҌ", + "require_password_change_on_login": "Зобов'ŅĐˇŅƒĐ˛Đ°Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŋĐĩŅ€ŅˆĐžĐŗĐž Đ˛Ņ…ĐžĐ´Ņƒ", + "reset_settings_to_default": "ĐĄĐēиĐŊŅƒŅ‚Đ¸ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Đ´Đž Ņ‚Đ¸ĐŋĐžĐ˛Đ¸Ņ…", "reset_settings_to_recent_saved": "ĐĄĐēиĐŊŅƒŅ‚Đ¸ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Đ´Đž ĐŊĐĩдавĐŊĐž СйĐĩŅ€ĐĩĐļĐĩĐŊĐ¸Ņ… ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ", "scanning_library": "ĐĄĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", "search_jobs": "ĐŸĐžŅˆŅƒĐē СавдаĐŊҌâ€Ļ", @@ -321,130 +321,130 @@ "server_welcome_message_description": "ĐŸĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊĐŊŅ, ŅĐēĐĩ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ”Ņ‚ŅŒŅŅ ĐŊа ŅŅ‚ĐžŅ€Ņ–ĐŊ҆Җ Đ˛Ņ…ĐžĐ´Ņƒ.", "settings_page_description": "ĐĄŅ‚ĐžŅ€Ņ–ĐŊĐēа ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", "sidecar_job": "МĐĩŅ‚Đ°Đ´Đ°ĐŊŅ– С sidecar-Ņ„Đ°ĐšĐģŅ–Đ˛", - "sidecar_job_description": "ĐŸĐžŅˆŅƒĐē айО ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ ŅĐ°ĐšĐ´ĐēĐ°Ņ€-ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ… С Ņ„Đ°ĐšĐģĐžĐ˛ĐžŅ— ŅĐ¸ŅŅ‚ĐĩĐŧи", + "sidecar_job_description": "ĐŸĐžŅˆŅƒĐē айО ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ sidecar-ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ… С Ņ„Đ°ĐšĐģĐžĐ˛ĐžŅ— ŅĐ¸ŅŅ‚ĐĩĐŧи", "slideshow_duration_description": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ҁĐĩĐē҃ĐŊĐ´ Đ´ĐģŅ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐēĐžĐļĐŊĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", - "smart_search_job_description": "РОСĐŋŅ–ĐˇĐŊĐ°Ņ” вĐŧҖҁ҂ Ņ„Đ°ĐšĐģŅ–Đ˛ Đ´ĐģŅ Ņ€ĐžĐˇŅƒĐŧĐŊĐžĐŗĐž ĐŋĐžŅˆŅƒĐē҃", - "storage_template_date_time_description": "Đ”Đ°Ņ‚ĐžŅŽ Ņ‚Đ° Ņ‡Đ°ŅĐžĐŧ Ņ” ĐŋОСĐŊĐ°Ņ‡Đēа Ņ‡Đ°ŅŅƒ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ Ņ„Đ°ĐšĐģ҃", - "storage_template_date_time_sample": "Đ§Đ°Ņ Đ˛Đ¸ĐąŅ–Ņ€Đēи {date}", - "storage_template_enable_description": "Đ’Đ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐŧĐĩŅ…Đ°ĐŊŅ–ĐˇĐŧ ŅˆĐ°ĐąĐģĐžĐŊŅ–Đ˛ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", + "smart_search_job_description": "ВиĐēĐžĐŊаĐŊĐŊŅ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ ĐŊа ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°Ņ… Ņ‰ĐžĐą ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒĐ˛Đ°Ņ‚Đ¸ Ņ€ĐžĐˇŅƒĐŧĐŊиК ĐŋĐžŅˆŅƒĐē", + "storage_template_date_time_description": "ДĐģŅ виСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ Đ´Đ°Ņ‚Đ¸ Ņ– Ņ‡Đ°ŅŅƒ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ ĐŧŅ–Ņ‚Đēа Ņ‡Đ°ŅŅƒ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°", + "storage_template_date_time_sample": "ĐŸŅ€Đ¸ĐēĐģад Ņ‡Đ°ŅŅƒ {date}", + "storage_template_enable_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐŧĐĩŅ…Đ°ĐŊŅ–ĐˇĐŧ ŅˆĐ°ĐąĐģĐžĐŊŅ–Đ˛ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", "storage_template_hash_verification_enabled": "ĐŖĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đē҃ Ņ…Đĩ҈҃", - "storage_template_hash_verification_enabled_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đē҃ Ņ…ĐĩŅˆĐ°. НĐĩ виĐŧиĐēĐ°ĐšŅ‚Đĩ ҆Đĩ, ŅĐēŅ‰Đž ви ĐŊĐĩ вĐŋĐĩвĐŊĐĩĐŊŅ– в ĐŊĐ°ŅĐģŅ–Đ´ĐēĐ°Ņ…", + "storage_template_hash_verification_enabled_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đē҃ Ņ…Đĩ҈҃. НĐĩ виĐŧиĐēĐ°ĐšŅ‚Đĩ ҆Đĩ, ŅĐēŅ‰Đž ви ĐŊĐĩ вĐŋĐĩвĐŊĐĩĐŊŅ– в ĐŊĐ°ŅĐģŅ–Đ´ĐēĐ°Ņ…", "storage_template_migration": "ĐœŅ–ĐŗŅ€Đ°Ņ†Ņ–Ņ ŅˆĐ°ĐąĐģĐžĐŊŅ–Đ˛ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", - "storage_template_migration_description": "Đ—Đ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžŅ‚ĐžŅ‡ĐŊиК {template} Đ´Đž Ņ€Đ°ĐŊŅ–ŅˆĐĩ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", - "storage_template_migration_info": "ШайĐģĐžĐŊ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ ĐēĐžĐŊвĐĩŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ĐŧĐĩ Đ˛ŅŅ– Ņ€ĐžĐˇŅˆĐ¸Ņ€ĐĩĐŊĐŊŅ ҃ ĐŊиĐļĐŊŅ–Đš Ņ€ĐĩĐŗŅ–ŅŅ‚Ņ€. ЗĐŧŅ–ĐŊи ŅˆĐ°ĐąĐģĐžĐŊ҃ ĐˇĐ°ŅŅ‚ĐžŅĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ Đ´Đž ĐŊĐžĐ˛Đ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛. ЊОй ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ Đ´Đž Ņ€Đ°ĐŊŅ–ŅˆĐĩ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛, СаĐŋŅƒŅŅ‚Ņ–Ņ‚ŅŒ {job}.", - "storage_template_migration_job": "ЗавдаĐŊĐŊŅ ĐŧŅ–ĐŗŅ€Đ°Ņ†Ņ–Ņ— ŅˆĐ°ĐąĐģĐžĐŊ҃ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ", - "storage_template_more_details": "ДĐģŅ ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ Đ´ĐĩŅ‚Đ°ĐģҌĐŊŅ–ŅˆĐžŅ— Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ— ĐŋŅ€Đž Ņ†ŅŽ Ņ„ŅƒĐŊĐēŅ†Ņ–ŅŽ, СвĐĩŅ€Ņ‚Đ°ĐšŅ‚ĐĩҁҌ Đ´Đž ШайĐģĐžĐŊ҃ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ Ņ‚Đ° ĐšĐžĐŗĐž ĐŊĐ°ŅĐģŅ–Đ´ĐēŅ–Đ˛", - "storage_template_onboarding_description_v2": "Đ¯ĐēŅ‰Đž Ņ†ŅŽ Ņ„ŅƒĐŊĐēŅ†Ņ–ŅŽ ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž, Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž вĐŋĐžŅ€ŅĐ´ĐēĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ Са ŅˆĐ°ĐąĐģĐžĐŊĐžĐŧ, виСĐŊĐ°Ņ‡ĐĩĐŊиĐŧ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ĐĩĐŧ. ДоĐēĐģадĐŊŅ–ŅˆĐĩ Đ´Đ¸Đ˛Ņ–Ņ‚ŅŒŅŅ в Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ–Ņ—.", + "storage_template_migration_description": "Đ—Đ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžŅ‚ĐžŅ‡ĐŊиК {template} Đ´Đž Ņ€Đ°ĐŊŅ–ŅˆĐĩ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", + "storage_template_migration_info": "ШайĐģĐžĐŊ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ° ĐŋĐĩŅ€ĐĩŅ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ĐŧĐĩ Đ˛ŅŅ– Ņ€ĐžĐˇŅˆĐ¸Ņ€ĐĩĐŊĐŊŅ ĐŊа ĐŊиĐļĐŊŅ–Đš Ņ€ĐĩĐŗŅ–ŅŅ‚Ņ€. ЗĐŧŅ–ĐŊи ŅˆĐ°ĐąĐģĐžĐŊ҃ ĐˇĐ°ŅŅ‚ĐžŅĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ Đ´Đž ĐŊĐžĐ˛Đ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛. ЊОй ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ Đ´Đž Ņ€Đ°ĐŊŅ–ŅˆĐĩ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛, виĐēĐžĐŊĐ°ĐšŅ‚Đĩ {job}.", + "storage_template_migration_job": "ЗавдаĐŊĐŊŅ ĐŧŅ–ĐŗŅ€Đ°Ņ†Ņ–Ņ— ŅˆĐ°ĐąĐģĐžĐŊ҃ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", + "storage_template_more_details": "ЊОй Đ´Ņ–ĐˇĐŊĐ°Ņ‚Đ¸ŅŅ ĐąŅ–ĐģҌ҈Đĩ ĐŋŅ€Đž Ņ†ŅŽ Ņ„ŅƒĐŊĐēŅ†Ņ–ŅŽ, СвĐĩŅ€ĐŊŅ–Ņ‚ŅŒŅŅ Đ´Đž ШайĐģĐžĐŊ҃ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ° Ņ‚Đ° ĐšĐžĐŗĐž ĐŊĐ°ŅĐģŅ–Đ´ĐēŅ–Đ˛", + "storage_template_onboarding_description_v2": "Đ¯ĐēŅ‰Đž Ņ†ŅŽ Ņ„ŅƒĐŊĐēŅ†Ņ–ŅŽ ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž, Ņ„Đ°ĐšĐģи Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž вĐŋĐžŅ€ŅĐ´ĐēĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ Са ŅˆĐ°ĐąĐģĐžĐŊĐžĐŧ, виСĐŊĐ°Ņ‡ĐĩĐŊиĐŧ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ĐĩĐŧ. ДоĐēĐģадĐŊŅ–ŅˆĐĩ Đ´Đ¸Đ˛Ņ–Ņ‚ŅŒŅŅ в Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ–Ņ—.", "storage_template_path_length": "ĐŸŅ€Đ¸ĐąĐģиСĐŊа ĐŧаĐēŅĐ¸ĐŧаĐģҌĐŊа дОвĐļиĐŊа ҈ĐģŅŅ…Ņƒ: {length, number}/{limit, number}", "storage_template_settings": "ШайĐģĐžĐŊ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", - "storage_template_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ŅŅ‚Ņ€ŅƒĐēŅ‚ŅƒŅ€ĐžŅŽ ĐŋаĐŋĐžĐē Ņ‚Đ° Ņ–ĐŧĐĩĐŊаĐŧи виваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", - "storage_template_user_label": "{label} - ҆Đĩ ĐŧŅ–Ņ‚Đēа СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", + "storage_template_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ŅŅ‚Ņ€ŅƒĐēŅ‚ŅƒŅ€ĐžŅŽ ĐŋаĐŋĐžĐē Ņ‚Đ° ĐŊаСваĐŧи Ņ„Đ°ĐšĐģŅ–Đ˛ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", + "storage_template_user_label": "{label} — ҆Đĩ ĐŧŅ–Ņ‚Đēа ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ° ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "system_settings": "ĐĄĐ¸ŅŅ‚ĐĩĐŧĐŊŅ– ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", "tag_cleanup_job": "ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐŊŅ Ņ‚ĐĩĐŗŅ–Đ˛", - "template_email_available_tags": "Ви ĐŧĐžĐļĐĩŅ‚Đĩ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊŅ– СĐŧŅ–ĐŊĐŊŅ– ҃ ŅĐ˛ĐžŅ”Đŧ҃ ŅˆĐ°ĐąĐģĐžĐŊŅ–: {tags}", - "template_email_if_empty": "Đ¯ĐēŅ‰Đž ŅˆĐ°ĐąĐģĐžĐŊ ĐŋĐžŅ€ĐžĐļĐŊŅ–Đš, ĐąŅƒĐ´Đĩ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐž ŅŅ‚Đ°ĐŊĐ´Đ°Ņ€Ņ‚ĐŊиК ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊиК ĐģĐ¸ŅŅ‚.", + "template_email_available_tags": "Ви ĐŧĐžĐļĐĩŅ‚Đĩ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ‚Đ°ĐēŅ– СĐŧŅ–ĐŊĐŊŅ– ҃ ŅĐ˛ĐžŅ”Đŧ҃ ŅˆĐ°ĐąĐģĐžĐŊŅ–: {tags}", + "template_email_if_empty": "Đ¯ĐēŅ‰Đž ŅˆĐ°ĐąĐģĐžĐŊ ĐŋĐžŅ€ĐžĐļĐŊŅ–Đš, ĐąŅƒĐ´Đĩ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐž Ņ‚Đ¸ĐŋОвиК ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊиК ĐģĐ¸ŅŅ‚.", "template_email_invite_album": "ШайĐģĐžĐŊ СаĐŋŅ€ĐžŅˆĐĩĐŊĐŊŅ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", - "template_email_preview": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´", + "template_email_preview": "ПоĐŋĐĩŅ€ĐĩĐ´ĐŊŅ–Đš ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´", "template_email_settings": "ШайĐģĐžĐŊи ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐ¸Ņ… ĐģĐ¸ŅŅ‚Ņ–Đ˛", - "template_email_update_album": "ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ аĐģŅŒĐąĐžĐŧ҃", + "template_email_update_album": "ШайĐģĐžĐŊ ĐžĐŊОвĐģĐĩĐŊĐŊŅ аĐģŅŒĐąĐžĐŧ҃", "template_email_welcome": "ШайĐģĐžĐŊ Đ˛Ņ–Ņ‚Đ°ĐģҌĐŊĐžĐŗĐž ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžĐŗĐž ĐģĐ¸ŅŅ‚Đ°", - "template_settings": "ШайĐģĐžĐŊи ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊҌ", - "template_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊаĐŧи Đ´ĐģŅ ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊҌ", - "theme_custom_css_settings": "ВĐģĐ°ŅĐŊиК CSS", - "theme_custom_css_settings_description": "ĐšĐ°ŅĐēадĐŊŅ– Ņ‚Đ°ĐąĐģĐ¸Ņ†Ņ– ŅŅ‚Đ¸ĐģŅ–Đ˛ дОСвОĐģŅŅŽŅ‚ŅŒ ĐŊĐ°ŅŅ‚Ņ€ĐžŅŽĐ˛Đ°Ņ‚Đ¸ диСаКĐŊ Immich.", - "theme_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ‚ĐĩĐŧи", - "theme_settings_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐĩŅ€ŅĐžĐŊаĐģŅ–ĐˇĐ°Ņ†Ņ–Ņ— вĐĩĐą-Ņ–ĐŊŅ‚ĐĩҀ҄ĐĩĐšŅŅƒ Immich", + "template_settings": "ШайĐģĐžĐŊи ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊҌ", + "template_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ Đ´ĐžĐ˛Ņ–ĐģҌĐŊиĐŧи ŅˆĐ°ĐąĐģĐžĐŊаĐŧи Đ´ĐģŅ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊҌ", + "theme_custom_css_settings": "Đ”ĐžĐ˛Ņ–ĐģҌĐŊиК CSS", + "theme_custom_css_settings_description": "ĐšĐ°ŅĐēадĐŊŅ– Ņ‚Đ°ĐąĐģĐ¸Ņ†Ņ– ŅŅ‚Đ¸ĐģŅ–Đ˛ Đ´Đ°ŅŽŅ‚ŅŒ СĐŧĐžĐŗŅƒ ĐŊаĐģĐ°ŅˆŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ диСаКĐŊ Immich.", + "theme_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ‚ĐĩĐŧи ĐžŅ„ĐžŅ€ĐŧĐģĐĩĐŊĐŊŅ", + "theme_settings_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛Đ¸ĐŗĐģŅĐ´Ņƒ вĐĩĐą-Ņ–ĐŊŅ‚ĐĩҀ҄ĐĩĐšŅŅƒ Immich", "thumbnail_generation_job": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€", - "thumbnail_generation_job_description": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ вĐĩĐģиĐēŅ–, ĐŧаĐģŅ– Ņ‚Đ° Ņ€ĐžĐˇĐŧĐ¸Ņ‚Ņ– ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Đ´ĐģŅ ĐēĐžĐļĐŊĐžĐŗĐž Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž, а Ņ‚Đ°ĐēĐžĐļ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Đ´ĐģŅ ĐēĐžĐļĐŊĐžŅ— ĐžŅĐžĐąĐ¸", + "thumbnail_generation_job_description": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ вĐĩĐģиĐēŅ–, ĐŧаĐģŅ– Ņ‚Đ° Ņ€ĐžĐˇĐŧĐ¸Ņ‚Ņ– ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Đ´ĐģŅ ĐēĐžĐļĐŊĐžĐŗĐž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°, а Ņ‚Đ°ĐēĐžĐļ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Đ´ĐģŅ ĐēĐžĐļĐŊĐžŅ— ĐģŅŽĐ´Đ¸ĐŊи", "transcoding_acceleration_api": "API ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅ", - "transcoding_acceleration_api_description": "API, ŅĐēа ĐąŅƒĐ´Đĩ Đ˛ĐˇĐ°Ņ”ĐŧĐžĐ´Ņ–ŅŅ‚Đ¸ С Đ˛Đ°ŅˆĐ¸Đŧ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ”Đŧ Đ´ĐģŅ ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. ĐĻĐĩ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋŅ€Đ°Ņ†ŅŽŅ” ҃ \"ĐŊаКĐēŅ€Đ°Ņ‰Đ¸Ņ… ҃ĐŧĐžĐ˛Đ°Ņ…\" Ņ–, в Ņ€Đ°ĐˇŅ– ĐŊĐĩĐ˛Đ´Đ°Ņ‡Ņ–, ĐŋĐĩŅ€ĐĩКдĐĩ ĐŊа ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŊĐĩ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. ĐŸŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐēа VP9 ĐŧĐžĐļĐĩ айО ĐŊĐĩ ĐŧĐžĐļĐĩ ĐŋŅ€Đ°Ņ†ŅŽĐ˛Đ°Ņ‚Đ¸, СаĐģĐĩĐļĐŊĐž Đ˛Ņ–Đ´ Đ˛Đ°ŅˆĐžĐŗĐž ОйĐģадĐŊаĐŊĐŊŅ.", - "transcoding_acceleration_nvenc": "NVENC (виĐŧĐ°ĐŗĐ°Ņ” ĐŗŅ€Đ°Ņ„Ņ–Ņ‡ĐŊĐžĐŗĐž ĐŋŅ€ĐžŅ†ĐĩŅĐžŅ€Đ° NVIDIA)", - "transcoding_acceleration_qsv": "ШвидĐēа ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ (ĐŋĐžŅ‚Ņ€Ņ–ĐąĐĩĐŊ ĐŋŅ€ĐžŅ†ĐĩŅĐžŅ€ Intel 7-ĐŗĐž ĐŋĐžĐēĐžĐģŅ–ĐŊĐŊŅ айО ĐŊĐžĐ˛Ņ–ŅˆĐžŅ— вĐĩҀҁҖҗ)", - "transcoding_acceleration_rkmpp": "RKMPP (҂ҖĐģҌĐēи ĐŊа SOC Rockchip)", + "transcoding_acceleration_api_description": "API, ŅĐēиК ĐąŅƒĐ´Đĩ Đ˛ĐˇĐ°Ņ”ĐŧĐžĐ´Ņ–ŅŅ‚Đ¸ С Đ˛Đ°ŅˆĐ¸Đŧ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ”Đŧ Đ´ĐģŅ ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. ĐĻĐĩ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊĐĩ ĐŗĐ°Ņ€Đ°ĐŊŅ‚ŅƒŅ” Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚: ҃ Ņ€Đ°ĐˇŅ– ĐŊĐĩĐ˛Đ´Đ°Ņ‡Ņ– ĐąŅƒĐ´Đĩ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐž ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŊĐĩ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. ĐŸŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐēа VP9 ĐŧĐžĐļĐĩ ĐŋŅ€Đ°Ņ†ŅŽĐ˛Đ°Ņ‚Đ¸ айО ĐŊŅ–, СаĐģĐĩĐļĐŊĐž Đ˛Ņ–Đ´ Đ˛Đ°ŅˆĐžĐŗĐž ОйĐģадĐŊаĐŊĐŊŅ.", + "transcoding_acceleration_nvenc": "NVENC (ĐŋĐžŅ‚Ņ€ĐĩĐąŅƒŅ” ĐŗŅ€Đ°Ņ„Ņ–Ņ‡ĐŊĐžĐŗĐž ĐŋŅ€ĐžŅ†ĐĩŅĐžŅ€Đ° NVIDIA)", + "transcoding_acceleration_qsv": "Intel Quick Sync (ĐŋĐžŅ‚Ņ€Ņ–ĐąĐĩĐŊ ĐŋŅ€ĐžŅ†ĐĩŅĐžŅ€ Intel 7-ĐŗĐž ĐŋĐžĐēĐžĐģŅ–ĐŊĐŊŅ айО ĐŊĐžĐ˛Ņ–ŅˆĐ¸Đš)", + "transcoding_acceleration_rkmpp": "RKMPP (ĐģĐ¸ŅˆĐĩ ĐŊа SoC Rockchip)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "ĐŸŅ€Đ¸ĐšĐŊŅŅ‚Ņ– Đ°ŅƒĐ´Ņ–ĐžĐēОдĐĩĐēи", "transcoding_accepted_audio_codecs_description": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ Đ°ŅƒĐ´Ņ–ĐžĐēОдĐĩĐēи, ŅĐēŅ– ĐŊĐĩ ĐŋĐžŅ‚Ņ€ĐĩĐąŅƒŅŽŅ‚ŅŒ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ ĐŋĐĩвĐŊĐ¸Ņ… ĐŋĐžĐģŅ–Ņ‚Đ¸Đē Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ.", "transcoding_accepted_containers": "ĐŸŅ€Đ¸ĐšĐŊŅŅ‚Ņ– ĐēĐžĐŊŅ‚ĐĩĐšĐŊĐĩŅ€Đ¸", - "transcoding_accepted_containers_description": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ, ŅĐēŅ– Ņ„ĐžŅ€ĐŧĐ°Ņ‚Đ¸ ĐēĐžĐŊŅ‚ĐĩĐšĐŊĐĩŅ€Ņ–Đ˛ ĐŊĐĩ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž ĐŋĐĩŅ€ĐĩŅ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ в MP4. ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ ĐŋĐĩвĐŊĐ¸Ņ… ĐŋĐžĐģŅ–Ņ‚Đ¸Đē ĐŋĐĩŅ€ĐĩĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ.", + "transcoding_accepted_containers_description": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ, ŅĐēŅ– Ņ„ĐžŅ€ĐŧĐ°Ņ‚Đ¸ ĐēĐžĐŊŅ‚ĐĩĐšĐŊĐĩŅ€Ņ–Đ˛ ĐŊĐĩ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž ĐŋĐĩŅ€ĐĩĐŋаĐēŅƒĐ˛Đ°Ņ‚Đ¸ в MP4. ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ ĐŋĐĩвĐŊĐ¸Ņ… ĐŋĐžĐģŅ–Ņ‚Đ¸Đē Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ.", "transcoding_accepted_video_codecs": "ĐŸŅ€Đ¸ĐšĐŊŅŅ‚Ņ– Đ˛Ņ–Đ´ĐĩĐžĐēОдĐĩĐēи", "transcoding_accepted_video_codecs_description": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ Đ˛Ņ–Đ´ĐĩĐžĐēОдĐĩĐēи, ŅĐēŅ– ĐŊĐĩ ĐŋĐžŅ‚Ņ€ĐĩĐąŅƒŅŽŅ‚ŅŒ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ ĐŋĐĩвĐŊĐ¸Ņ… ĐŋĐžĐģŅ–Ņ‚Đ¸Đē Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ.", "transcoding_advanced_options_description": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸, ŅĐēŅ– ĐąŅ–ĐģŅŒŅˆĐžŅŅ‚Ņ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ ĐŊĐĩ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž СĐŧŅ–ĐŊŅŽĐ˛Đ°Ņ‚Đ¸", "transcoding_audio_codec": "ĐŅƒĐ´Ņ–ĐžĐēОдĐĩĐē", - "transcoding_audio_codec_description": "Opus - ҆Đĩ ĐžĐŋŅ†Ņ–Ņ ĐŊĐ°ĐšĐ˛Đ¸Ņ‰ĐžŅ— ŅĐēĐžŅŅ‚Ņ–, аĐģĐĩ ĐŧĐĩĐŊ҈Đĩ ҁ҃ĐŧҖҁĐŊа ĐˇŅ– ŅŅ‚Đ°Ņ€Đ¸Đŧи ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅĐŧи айО ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŊиĐŧ СайĐĩСĐŋĐĩ҇ĐĩĐŊĐŊŅĐŧ.", + "transcoding_audio_codec_description": "Opus — Đ˛Đ°Ņ€Ņ–Đ°ĐŊŅ‚ ĐŊĐ°ĐšĐ˛Đ¸Ņ‰ĐžŅ— ŅĐēĐžŅŅ‚Ņ–, аĐģĐĩ ĐŧĐ°Ņ” ĐŊиĐļŅ‡Ņƒ ҁ҃ĐŧҖҁĐŊŅ–ŅŅ‚ŅŒ ĐˇŅ– ŅŅ‚Đ°Ņ€Đ¸Đŧи ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅĐŧи айО ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧаĐŧи.", "transcoding_bitrate_description": "Đ’Ņ–Đ´ĐĩĐž С ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚ĐžĐŧ Đ˛Đ¸Ņ‰Đĩ ĐŧаĐēŅĐ¸ĐŧаĐģҌĐŊĐžĐŗĐž айО ĐŊĐĩ в ĐŋŅ€Đ¸ĐšĐŊŅŅ‚ĐžĐŧ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ–", - "transcoding_codecs_learn_more": "ДĐģŅ ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛ĐžŅ— Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ— ĐŋŅ€Đž Ņ‚ĐĩŅ€ĐŧŅ–ĐŊĐžĐģĐžĐŗŅ–ŅŽ, Ņ‰Đž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ Ņ‚ŅƒŅ‚, СвĐĩŅ€Ņ‚Đ°ĐšŅ‚ĐĩŅŅ Đ´Đž Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ–Ņ— FFmpeg Đ´ĐģŅ ĐēОдĐĩĐēŅ–Đ˛ H.264, HEVC Ņ‚Đ° VP9.", + "transcoding_codecs_learn_more": "ЊОй Đ´Ņ–ĐˇĐŊĐ°Ņ‚Đ¸ŅŅ ĐąŅ–ĐģҌ҈Đĩ ĐŋŅ€Đž Ņ‚ĐĩŅ€ĐŧŅ–ĐŊĐžĐģĐžĐŗŅ–ŅŽ, Ņ‰Đž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ Ņ‚ŅƒŅ‚, СвĐĩŅ€Ņ‚Đ°ĐšŅ‚ĐĩŅŅ Đ´Đž Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ–Ņ— FFmpeg Đ´ĐģŅ ĐēОдĐĩĐēŅ–Đ˛ H.264, HEVC Ņ‚Đ° VP9.", "transcoding_constant_quality_mode": "Đ ĐĩĐļиĐŧ ĐŋĐžŅŅ‚Ņ–ĐšĐŊĐžŅ— ŅĐēĐžŅŅ‚Ņ–", - "transcoding_constant_quality_mode_description": "ICQ ĐēŅ€Đ°Ņ‰Đĩ, ĐŊŅ–Đļ CQP, аĐģĐĩ Đ´ĐĩŅĐēŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— аĐŋĐ°Ņ€Đ°Ņ‚ĐŊĐžĐŗĐž ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅ ĐŊĐĩ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅŽŅ‚ŅŒ ҆ĐĩĐš Ņ€ĐĩĐļиĐŧ. Đ’ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ Ņ†ŅŒĐžĐŗĐž ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ° ĐąŅƒĐ´Đĩ Đ˛Ņ–Đ´Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ СаСĐŊĐ°Ņ‡ĐĩĐŊĐžĐŧ҃ Ņ€ĐĩĐļиĐŧ҃ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊа ĐžŅĐŊĐžĐ˛Ņ– ŅĐēĐžŅŅ‚Ņ–. Đ†ĐŗĐŊĐžŅ€ŅƒŅ”Ņ‚ŅŒŅŅ NVENC, ĐžŅĐēŅ–ĐģҌĐēи Đ˛Ņ–ĐŊ ĐŊĐĩ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ” ICQ.", - "transcoding_constant_rate_factor": "КоĐĩ҄Җ҆ҖҔĐŊŅ‚ ĐŋĐžŅŅ‚Ņ–ĐšĐŊĐžŅ— ŅĐēĐžŅŅ‚Ņ– (-crf)", + "transcoding_constant_quality_mode_description": "ICQ СайĐĩСĐŋĐĩŅ‡ŅƒŅ” ĐēŅ€Đ°Ņ‰Ņƒ ŅĐēŅ–ŅŅ‚ŅŒ, ĐŊŅ–Đļ CQP, аĐģĐĩ Đ´ĐĩŅĐēŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— аĐŋĐ°Ņ€Đ°Ņ‚ĐŊĐžĐŗĐž ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅ ĐŊĐĩ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅŽŅ‚ŅŒ ҆ĐĩĐš Ņ€ĐĩĐļиĐŧ. ĐŖŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ Ņ†ŅŒĐžĐŗĐž ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ° ĐŊĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ĐŧĐĩ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ СаСĐŊĐ°Ņ‡ĐĩĐŊĐžĐŧ҃ Ņ€ĐĩĐļиĐŧ҃ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊа ĐžŅĐŊĐžĐ˛Ņ– ŅĐēĐžŅŅ‚Ņ–. NVENC Ņ–ĐŗĐŊĐžŅ€ŅƒŅ” ҆ĐĩĐš ĐŋĐ°Ņ€Đ°ĐŧĐĩ҂Ҁ, ĐžŅĐēŅ–ĐģҌĐēи ĐŊĐĩ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ” ICQ.", + "transcoding_constant_rate_factor": "ФаĐēŅ‚ĐžŅ€ ĐŋĐžŅŅ‚Ņ–ĐšĐŊĐžŅ— ŅĐēĐžŅŅ‚Ņ– (-crf)", "transcoding_constant_rate_factor_description": "Đ Ņ–Đ˛ĐĩĐŊҌ ŅĐēĐžŅŅ‚Ņ– Đ˛Ņ–Đ´ĐĩĐž. Đ—Đ°ĐˇĐ˛Đ¸Ņ‡Đ°Đš СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ Đ´ĐģŅ H.264 - 23, HEVC - 28, VP9 - 31, AV1 - 35. НиĐļ҇Đĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐēŅ€Đ°Ņ‰Đĩ, аĐģĐĩ ŅŅ‚Đ˛ĐžŅ€ŅŽŅ” ĐąŅ–ĐģŅŒŅˆŅ– Ņ„Đ°ĐšĐģи.", "transcoding_disabled_description": "БĐĩС Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛Ņ–Đ´ĐĩĐž — ĐŧĐžĐļĐĩ ĐŋŅ€Đ¸ĐˇĐ˛ĐĩŅŅ‚Đ¸ Đ´Đž ĐŋŅ€ĐžĐąĐģĐĩĐŧ С Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅĐŧ ĐŊа Đ´ĐĩŅĐēĐ¸Ņ… ĐēĐģŅ–Ņ”ĐŊŅ‚Đ°Ņ…", "transcoding_encoding_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ", "transcoding_encoding_options_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐēОдĐĩĐēŅ–Đ˛, Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅ— ĐˇĐ´Đ°Ņ‚ĐŊĐžŅŅ‚Ņ–, ŅĐēĐžŅŅ‚Ņ– Ņ‚Đ° Ņ–ĐŊŅˆĐ¸Ņ… ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Ņ–Đ˛ Đ´ĐģŅ ĐēОдОваĐŊĐ¸Ņ… Đ˛Ņ–Đ´ĐĩĐž", "transcoding_hardware_acceleration": "АĐŋĐ°Ņ€Đ°Ņ‚ĐŊĐĩ ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅ", - "transcoding_hardware_acceleration_description": "ЕĐēҁĐŋĐĩŅ€Đ¸ĐŧĐĩĐŊŅ‚Đ°ĐģҌĐŊĐž: ŅˆĐ˛Đ¸Đ´ŅˆĐĩ ĐŋĐĩŅ€ĐĩĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ, аĐģĐĩ ĐŧĐžĐļĐĩ СĐŊиĐļŅƒĐ˛Đ°Ņ‚Đ¸ ŅĐēŅ–ŅŅ‚ŅŒ ĐŋŅ€Đ¸ Ņ‚ĐžĐŧ҃ ŅĐ°ĐŧĐžĐŧ҃ ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚Ņ–", + "transcoding_hardware_acceleration_description": "ЕĐēҁĐŋĐĩŅ€Đ¸ĐŧĐĩĐŊŅ‚Đ°ĐģҌĐŊĐž: ŅˆĐ˛Đ¸Đ´ŅˆĐĩ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ, аĐģĐĩ ĐŧĐžĐļĐĩ СĐŊиĐļŅƒĐ˛Đ°Ņ‚Đ¸ ŅĐēŅ–ŅŅ‚ŅŒ Са Ņ‚ĐžĐŗĐž ŅĐ°ĐŧĐžĐŗĐž ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚Ņƒ", "transcoding_hardware_decoding": "АĐŋĐ°Ņ€Đ°Ņ‚ĐŊĐĩ Đ´ĐĩĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ", "transcoding_hardware_decoding_setting_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐŊŅ ĐŊĐ°ŅĐēŅ€Ņ–ĐˇĐŊĐžĐŗĐž ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅ СаĐŧŅ–ŅŅ‚ŅŒ ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅ ĐģĐ¸ŅˆĐĩ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. МоĐļĐĩ ĐŊĐĩ ĐŋŅ€Đ°Ņ†ŅŽĐ˛Đ°Ņ‚Đ¸ Đ´ĐģŅ Đ˛ŅŅ–Ņ… Đ˛Ņ–Đ´ĐĩĐž.", - "transcoding_max_b_frames": "МаĐēŅĐ¸ĐŧаĐģҌĐŊа ĐēŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ĐŋŅ€ĐžĐŧŅ–ĐļĐŊĐ¸Ņ… ĐēĐ°Đ´Ņ€Ņ–Đ˛", - "transcoding_max_b_frames_description": "Đ’Đ¸Ņ‰Ņ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŋĐžĐēŅ€Đ°Ņ‰ŅƒŅŽŅ‚ŅŒ ĐĩŅ„ĐĩĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ ŅŅ‚Đ¸ŅĐŊĐĩĐŊĐŊŅ, аĐģĐĩ ĐˇĐąŅ–ĐģŅŒŅˆŅƒŅŽŅ‚ŅŒ Ņ‡Đ°Ņ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. МоĐļŅƒŅ‚ŅŒ ĐąŅƒŅ‚Đ¸ ĐŊĐĩҁ҃ĐŧҖҁĐŊŅ– С аĐŋĐ°Ņ€Đ°Ņ‚ĐŊиĐŧ ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅĐŧ ĐŊа ŅŅ‚Đ°Ņ€Đ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŅ…. ЗĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ 0 виĐŧиĐēĐ°Ņ” B-҄ҀĐĩĐšĐŧи, а -1 Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž ĐŊаĐģĐ°ŅˆŅ‚ĐžĐ˛ŅƒŅ” ҆Đĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ.", + "transcoding_max_b_frames": "МаĐēŅĐ¸ĐŧаĐģҌĐŊа ĐēŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ B-ĐēĐ°Đ´Ņ€Ņ–Đ˛", + "transcoding_max_b_frames_description": "Đ’Đ¸Ņ‰Ņ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŋĐžĐēŅ€Đ°Ņ‰ŅƒŅŽŅ‚ŅŒ ĐĩŅ„ĐĩĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ ŅŅ‚Đ¸ŅĐŊĐĩĐŊĐŊŅ, аĐģĐĩ ĐˇĐąŅ–ĐģŅŒŅˆŅƒŅŽŅ‚ŅŒ Ņ‡Đ°Ņ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. МоĐļŅƒŅ‚ŅŒ ĐąŅƒŅ‚Đ¸ ĐŊĐĩҁ҃ĐŧҖҁĐŊŅ– С аĐŋĐ°Ņ€Đ°Ņ‚ĐŊиĐŧ ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅĐŧ ĐŊа ŅŅ‚Đ°Ņ€Đ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŅ…. ЗĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ 0 виĐŧиĐēĐ°Ņ” B-ĐēĐ°Đ´Ņ€Đ¸, а -1 виСĐŊĐ°Ņ‡Đ°Ņ” ҆ĐĩĐš ĐŋĐ°Ņ€Đ°ĐŧĐĩ҂Ҁ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž.", "transcoding_max_bitrate": "МаĐēŅĐ¸ĐŧаĐģҌĐŊиК ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚", - "transcoding_max_bitrate_description": "Đ’ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ ĐŧаĐēŅĐ¸ĐŧаĐģҌĐŊĐžŅ— ŅˆĐ˛Đ¸Đ´ĐēĐžŅŅ‚Ņ– ĐŋĐĩŅ€ĐĩĐ´Đ°Ņ‡Ņ– даĐŊĐ¸Ņ… ĐŧĐžĐļĐĩ ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸ Ņ€ĐžĐˇĐŧŅ–Ņ€Đ¸ Ņ„Đ°ĐšĐģŅ–Đ˛ ĐąŅ–ĐģҌ҈ ĐŋĐĩŅ€ĐĩĐ´ĐąĐ°Ņ‡ŅƒĐ˛Đ°ĐŊиĐŧи Са ĐŊĐĩСĐŊĐ°Ņ‡ĐŊĐžŅ— Đ˛Ņ‚Ņ€Đ°Ņ‚Đ¸ ŅĐēĐžŅŅ‚Ņ–. ĐŸŅ€Đ¸ Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊŅ–Đš ĐˇĐ´Đ°Ņ‚ĐŊĐžŅŅ‚Ņ– 720p Ņ‚Đ¸ĐŋĐžĐ˛Ņ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ŅŅ‚Đ°ĐŊОвĐģŅŅ‚ŅŒ 2600 ĐēĐąŅ–Ņ‚/ҁ Đ´ĐģŅ VP9 айО HEVC, айО 4500 ĐēĐąŅ–Ņ‚/ҁ Đ´ĐģŅ H.264. ВиĐŧĐēĐŊĐĩĐŊĐž, ŅĐēŅ‰Đž Đ˛ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ 0. Đ¯ĐēŅ‰Đž ОдиĐŊĐ¸Ņ†Ņ виĐŧŅ–Ņ€Ņƒ ĐŊĐĩ вĐēаСаĐŊа, ĐŋŅ€Đ¸ĐšĐŧĐ°Ņ”Ņ‚ŅŒŅŅ k (Đ´ĐģŅ ĐēĐąŅ–Ņ‚/ҁ); ĐžŅ‚ĐļĐĩ, 5000, 5000k Ņ– 5M (Đ´ĐģŅ ĐœĐąŅ–Ņ‚/ҁ) Ņ” ĐĩĐēĐ˛Ņ–Đ˛Đ°ĐģĐĩĐŊŅ‚ĐŊиĐŧи.", + "transcoding_max_bitrate_description": "ĐŖŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ ĐŧаĐēŅĐ¸ĐŧаĐģҌĐŊĐžĐŗĐž ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚Ņƒ ĐŧĐžĐļĐĩ ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸ Ņ€ĐžĐˇĐŧŅ–Ņ€ Ņ„Đ°ĐšĐģŅ–Đ˛ ĐąŅ–ĐģҌ҈ ĐŋĐĩŅ€ĐĩĐ´ĐąĐ°Ņ‡ŅƒĐ˛Đ°ĐŊиĐŧ Са ĐŊĐĩСĐŊĐ°Ņ‡ĐŊĐžŅ— Đ˛Ņ‚Ņ€Đ°Ņ‚Đ¸ ŅĐēĐžŅŅ‚Ņ–. За Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅ— ĐˇĐ´Đ°Ņ‚ĐŊĐžŅŅ‚Ņ– 720p Ņ‚Đ¸ĐŋĐžĐ˛Ņ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ŅŅ‚Đ°ĐŊОвĐģŅŅ‚ŅŒ 2600 ĐēĐąŅ–Ņ‚/ҁ Đ´ĐģŅ VP9 айО HEVC, айО 4500 ĐēĐąŅ–Ņ‚/ҁ Đ´ĐģŅ H.264. ВиĐŧĐēĐŊĐĩĐŊĐž, ŅĐēŅ‰Đž ŅƒŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ 0. Đ¯ĐēŅ‰Đž ОдиĐŊĐ¸Ņ†Ņ виĐŧŅ–Ņ€Ņƒ ĐŊĐĩ вĐēаСаĐŊа, ĐŋŅ€Đ¸ĐšĐŧĐ°Ņ”Ņ‚ŅŒŅŅ k (Đ´ĐģŅ ĐēĐąŅ–Ņ‚/ҁ); ĐžŅ‚ĐļĐĩ, 5000, 5000k Ņ– 5M (Đ´ĐģŅ ĐœĐąŅ–Ņ‚/ҁ) Ņ” ĐĩĐēĐ˛Ņ–Đ˛Đ°ĐģĐĩĐŊŅ‚ĐŊиĐŧи.", "transcoding_max_keyframe_interval": "МаĐēŅĐ¸ĐŧаĐģҌĐŊиК Ņ–ĐŊŅ‚ĐĩŅ€Đ˛Đ°Đģ ĐēĐģŅŽŅ‡ĐžĐ˛Đ¸Ņ… ĐēĐ°Đ´Ņ€Ņ–Đ˛", - "transcoding_max_keyframe_interval_description": "Đ’ŅŅ‚Đ°ĐŊОвĐģŅŽŅ” ĐŧаĐēŅĐ¸ĐŧаĐģҌĐŊ҃ Đ˛Ņ–Đ´ŅŅ‚Đ°ĐŊҌ ĐŧŅ–Đļ ĐēĐģŅŽŅ‡ĐžĐ˛Đ¸Đŧи ĐēĐ°Đ´Ņ€Đ°Đŧи. НиĐļ҇Җ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŋĐžĐŗŅ–Ņ€ŅˆŅƒŅŽŅ‚ŅŒ ĐĩŅ„ĐĩĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ ŅŅ‚Đ¸ŅĐŊĐĩĐŊĐŊŅ, аĐģĐĩ ĐŋĐžĐēŅ€Đ°Ņ‰ŅƒŅŽŅ‚ŅŒ Ņ‡Đ°Ņ ĐŋĐžŅˆŅƒĐē҃ Ņ– ĐŧĐžĐļŅƒŅ‚ŅŒ ĐŋĐžĐēŅ€Đ°Ņ‰Đ¸Ņ‚Đ¸ ŅĐēŅ–ŅŅ‚ŅŒ в ҁ҆ĐĩĐŊĐ°Ņ… С ŅˆĐ˛Đ¸Đ´ĐēиĐŧи Ņ€ŅƒŅ…Đ°Đŧи. ЗĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ 0 Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž Đ˛ŅŅ‚Đ°ĐŊОвĐģŅŽŅ” ҆Đĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ.", + "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. Đ’ŅŅ‚Đ°ĐŊОвĐģŅŽŅ” Đ˛ŅƒĐˇĐžĐģ DRI, ŅĐēиК виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ Đ´ĐģŅ аĐŋĐ°Ņ€Đ°Ņ‚ĐŊĐžĐŗĐž Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ.", - "transcoding_preset_preset": "ĐŸĐ°Ņ€Đ°ĐŧĐĩ҂Ҁ (-preset)", - "transcoding_preset_preset_description": "ШвидĐēŅ–ŅŅ‚ŅŒ ŅŅ‚Đ¸ŅĐŊĐĩĐŊĐŊŅ. ĐŸĐžĐ˛Ņ–ĐģҌĐŊŅ–ŅˆŅ– ĐŋŅ€ĐĩҁĐĩŅ‚Đ¸ ŅŅ‚Đ˛ĐžŅ€ŅŽŅŽŅ‚ŅŒ ĐŧĐĩĐŊŅˆŅ– Ņ„Đ°ĐšĐģи Ņ– ĐŋŅ–Đ´Đ˛Đ¸Ņ‰ŅƒŅŽŅ‚ŅŒ ŅĐēŅ–ŅŅ‚ŅŒ ĐŋŅ€Đ¸ ĐŋĐĩвĐŊĐžĐŧ҃ ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚Ņ–. VP9 Ņ–ĐŗĐŊĐžŅ€ŅƒŅ” ŅˆĐ˛Đ¸Đ´ĐēĐžŅŅ‚Ņ– Đ˛Đ¸Ņ‰Đĩ 'ŅˆĐ˛Đ¸Đ´ŅˆĐĩ'.", - "transcoding_reference_frames": "ĐžŅĐŊОвĐŊŅ– ĐēĐ°Đ´Ņ€Đ¸", - "transcoding_reference_frames_description": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ĐēĐ°Đ´Ņ€Ņ–Đ˛, ĐŊа ŅĐēŅ– ĐŋĐžŅĐ¸ĐģĐ°Ņ”Ņ‚ŅŒŅŅ ĐŋŅ€Đ¸ ŅŅ‚Đ¸ŅĐŊĐĩĐŊĐŊŅ– даĐŊĐžĐŗĐž ĐēĐ°Đ´Ņ€Ņƒ. Đ’Đ¸Ņ‰Ņ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŋĐžĐēŅ€Đ°Ņ‰ŅƒŅŽŅ‚ŅŒ ĐĩŅ„ĐĩĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ ŅŅ‚Đ¸ŅĐŊĐĩĐŊĐŊŅ, аĐģĐĩ ĐˇĐąŅ–ĐģŅŒŅˆŅƒŅŽŅ‚ŅŒ Ņ‡Đ°Ņ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. ЗĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ 0 Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž ĐŊаĐģĐ°ŅˆŅ‚ĐžĐ˛ŅƒŅ” ҆Đĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ.", - "transcoding_required_description": "Đ›Đ¸ŅˆĐĩ Đ˛Ņ–Đ´ĐĩĐž, Ņ‰Đž ĐŊĐĩ ҃ ĐŋŅ€Đ¸ĐšĐŊŅŅ‚ĐžĐŧ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ–", + "transcoding_preferred_hardware_device": "БаĐļаĐŊиК аĐŋĐ°Ņ€Đ°Ņ‚ĐŊиК ĐŋŅ€Đ¸ŅŅ‚Ņ€Ņ–Đš", + "transcoding_preferred_hardware_device_description": "Đ—Đ°ŅŅ‚ĐžŅĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ Đ´Đž VAAPI Ņ– QSV. ĐŖŅŅ‚Đ°ĐŊОвĐģŅŽŅ” Đ˛ŅƒĐˇĐžĐģ DRI, ŅĐēиК виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ Đ´ĐģŅ аĐŋĐ°Ņ€Đ°Ņ‚ĐŊĐžĐŗĐž Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ.", + "transcoding_preset_preset": "ĐŸŅ€ĐĩҁĐĩŅ‚ (-preset)", + "transcoding_preset_preset_description": "ШвидĐēŅ–ŅŅ‚ŅŒ ŅŅ‚Đ¸ŅĐŊĐĩĐŊĐŊŅ. ĐŸĐžĐ˛Ņ–ĐģҌĐŊŅ–ŅˆŅ– ĐŋŅ€ĐĩҁĐĩŅ‚Đ¸ ŅŅ‚Đ˛ĐžŅ€ŅŽŅŽŅ‚ŅŒ ĐŧĐĩĐŊŅˆŅ– Ņ„Đ°ĐšĐģи Ņ– ĐŋŅ–Đ´Đ˛Đ¸Ņ‰ŅƒŅŽŅ‚ŅŒ ŅĐēŅ–ŅŅ‚ŅŒ Са ĐŋĐĩвĐŊĐžĐŗĐž ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚Ņƒ. VP9 Ņ–ĐŗĐŊĐžŅ€ŅƒŅ” ŅˆĐ˛Đ¸Đ´ĐēĐžŅŅ‚Ņ– Đ˛Đ¸Ņ‰Đĩ 'faster'.", + "transcoding_reference_frames": "ОĐŋĐžŅ€ĐŊŅ– ĐēĐ°Đ´Ņ€Đ¸", + "transcoding_reference_frames_description": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ĐēĐ°Đ´Ņ€Ņ–Đ˛, ĐŊа ŅĐēŅ– ĐŋĐžŅĐ¸ĐģĐ°Ņ”Ņ‚ŅŒŅŅ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ŅŅ‚Đ¸ŅĐŊĐĩĐŊĐŊŅ ĐŋĐĩвĐŊĐžĐŗĐž ĐēĐ°Đ´Ņ€Ņƒ. Đ’Đ¸Ņ‰Ņ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŋĐžĐēŅ€Đ°Ņ‰ŅƒŅŽŅ‚ŅŒ ĐĩŅ„ĐĩĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ ŅŅ‚Đ¸ŅĐŊĐĩĐŊĐŊŅ, аĐģĐĩ ĐˇĐąŅ–ĐģŅŒŅˆŅƒŅŽŅ‚ŅŒ Ņ‡Đ°Ņ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. ЗĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ 0 виСĐŊĐ°Ņ‡Đ°Ņ” ҆ĐĩĐš ĐŋĐ°Ņ€Đ°ĐŧĐĩ҂Ҁ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž.", + "transcoding_required_description": "Đ›Đ¸ŅˆĐĩ Đ˛Ņ–Đ´ĐĩĐž, Ņ‰Đž ĐŊĐĩ ĐŧĐ°ŅŽŅ‚ŅŒ ĐŋŅ€Đ¸ĐšĐŊŅŅ‚ĐŊĐžĐŗĐž Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņƒ", "transcoding_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛Ņ–Đ´ĐĩĐž", - "transcoding_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ŅĐēŅ– Đ˛Ņ–Đ´ĐĩĐž Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ– ŅĐē Ņ—Ņ… ĐžĐąŅ€ĐžĐąĐģŅŅ‚Đ¸", - "transcoding_target_resolution": "Đ ĐžĐˇĐ´Ņ–ĐģҌĐŊа ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ", + "transcoding_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ‚Đ¸Đŧ, ŅĐēŅ– Đ˛Ņ–Đ´ĐĩĐž Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ– ŅĐē Ņ—Ņ… ĐžĐąŅ€ĐžĐąĐģŅŅ‚Đ¸", + "transcoding_target_resolution": "ĐĻŅ–ĐģŅŒĐžĐ˛Đ° Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊа ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ", "transcoding_target_resolution_description": "Đ’Đ¸Ņ‰Ņ– Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊŅ– ĐˇĐ´Đ°Ņ‚ĐŊĐžŅŅ‚Ņ– ĐŧĐžĐļŅƒŅ‚ŅŒ СйĐĩŅ€Ņ–ĐŗĐ°Ņ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ Đ´ĐĩŅ‚Đ°ĐģĐĩĐš, аĐģĐĩ СаКĐŧĐ°ŅŽŅ‚ŅŒ ĐąŅ–ĐģҌ҈Đĩ Ņ‡Đ°ŅŅƒ ĐŊа ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ, ĐŧĐ°ŅŽŅ‚ŅŒ ĐąŅ–ĐģŅŒŅˆŅ– Ņ€ĐžĐˇĐŧŅ–Ņ€Đ¸ Ņ„Đ°ĐšĐģŅ–Đ˛ Ņ– ĐŧĐžĐļŅƒŅ‚ŅŒ СĐŧĐĩĐŊŅˆĐ¸Ņ‚Đ¸ ŅˆĐ˛Đ¸Đ´ĐēŅ–ŅŅ‚ŅŒ Ņ€ĐžĐąĐžŅ‚Đ¸ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃.", - "transcoding_temporal_aq": "ĐĸиĐŧŅ‡Đ°ŅĐžĐ˛Đĩ AQ", - "transcoding_temporal_aq_description": "ĐĄŅ‚ĐžŅŅƒŅ”Ņ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ NVENC. Đ§Đ°ŅĐžĐ˛Đ° адаĐŋŅ‚Đ¸Đ˛ĐŊа ĐēваĐŊŅ‚Đ¸ĐˇĐ°Ņ†Ņ–Ņ ĐŋŅ–Đ´Đ˛Đ¸Ņ‰ŅƒŅ” ŅĐēŅ–ŅŅ‚ŅŒ ҁ҆ĐĩĐŊ С Đ˛Đ¸ŅĐžĐēĐžŅŽ Đ´ĐĩŅ‚Đ°ĐģŅ–ĐˇĐ°Ņ†Ņ–Ņ”ŅŽ Ņ‚Đ° ĐŊĐ¸ĐˇŅŒĐēиĐŧ Ņ€Ņ–Đ˛ĐŊĐĩĐŧ Ņ€ŅƒŅ…Ņƒ. МоĐļĐĩ ĐąŅƒŅ‚Đ¸ ĐŊĐĩҁ҃ĐŧҖҁĐŊиĐŧ ĐˇŅ– ŅŅ‚Đ°Ņ€Đ¸Đŧи ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅĐŧи.", + "transcoding_temporal_aq": "Đ§Đ°ŅĐžĐ˛Đĩ AQ", + "transcoding_temporal_aq_description": "ĐĄŅ‚ĐžŅŅƒŅ”Ņ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ NVENC. Đ§Đ°ŅĐžĐ˛Đ° адаĐŋŅ‚Đ¸Đ˛ĐŊа ĐēваĐŊŅ‚Đ¸ĐˇĐ°Ņ†Ņ–Ņ ĐŋŅ–Đ´Đ˛Đ¸Ņ‰ŅƒŅ” ŅĐēŅ–ŅŅ‚ŅŒ ҁ҆ĐĩĐŊ С Đ˛Đ¸ŅĐžĐēĐžŅŽ Đ´ĐĩŅ‚Đ°ĐģŅ–ĐˇĐ°Ņ†Ņ–Ņ”ŅŽ Ņ‚Đ° ĐŊĐ¸ĐˇŅŒĐēиĐŧ Ņ€Ņ–Đ˛ĐŊĐĩĐŧ Ņ€ŅƒŅ…Ņƒ. МоĐļĐĩ ĐąŅƒŅ‚Đ¸ ĐŊĐĩҁ҃ĐŧҖҁĐŊĐžŅŽ ĐˇŅ– ŅŅ‚Đ°Ņ€Đ¸Đŧи ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅĐŧи.", "transcoding_threads": "ĐŸĐžŅ‚ĐžĐēи", - "transcoding_threads_description": "Đ’Đ¸Ņ‰Ņ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŋŅ€Đ¸ŅĐēĐžŅ€ŅŽŅŽŅ‚ŅŒ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ, аĐģĐĩ СаĐģĐ¸ŅˆĐ°ŅŽŅ‚ŅŒ ĐŧĐĩĐŊ҈Đĩ ĐŧŅ–ŅŅ†Ņ Đ´ĐģŅ ĐžĐąŅ€ĐžĐąĐēи Ņ–ĐŊŅˆĐ¸Ņ… СавдаĐŊҌ ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ ĐŋŅ–Đ´ Ņ‡Đ°Ņ аĐēŅ‚Đ¸Đ˛ĐŊĐžŅŅ‚Ņ–. ĐĻĐĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŊĐĩ ĐŋОвиĐŊĐŊĐž ĐąŅƒŅ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ ĐēŅ–ĐģҌĐēĐžŅŅ‚Ņ– ŅĐ´ĐĩŅ€ ĐŋŅ€ĐžŅ†ĐĩŅĐžŅ€Đ°. МаĐēŅĐ¸ĐŧŅ–ĐˇŅƒŅ” виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ, ŅĐēŅ‰Đž Đ˛ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž ĐŊа 0.", + "transcoding_threads_description": "Đ’Đ¸Ņ‰Ņ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŋŅ€Đ¸ŅĐēĐžŅ€ŅŽŅŽŅ‚ŅŒ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ, аĐģĐĩ СаĐģĐ¸ŅˆĐ°ŅŽŅ‚ŅŒ ĐŧĐĩĐŊ҈Đĩ ĐŧŅ–ŅŅ†Ņ Đ´ĐģŅ ĐžĐąŅ€ĐžĐąĐēи Ņ–ĐŊŅˆĐ¸Ņ… СавдаĐŊҌ ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ ĐŋŅ–Đ´ Ņ‡Đ°Ņ аĐēŅ‚Đ¸Đ˛ĐŊĐžŅŅ‚Ņ–. ĐĻĐĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŊĐĩ ĐŧĐ°Ņ” ĐŋĐĩŅ€ĐĩĐ˛Đ¸Ņ‰ŅƒĐ˛Đ°Ņ‚Đ¸ ĐēŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ŅĐ´ĐĩŅ€ ĐŋŅ€ĐžŅ†ĐĩŅĐžŅ€Đ°. МаĐēŅĐ¸ĐŧŅ–ĐˇŅƒŅ” виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ, ŅĐēŅ‰Đž ŅƒŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž ĐŊа 0.", "transcoding_tone_mapping": "ĐĸĐžĐŊОвĐĩ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", - "transcoding_tone_mapping_description": "НаĐŧĐ°ĐŗĐ°Ņ”Ņ‚ŅŒŅŅ СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ Đ˛Đ¸ĐŗĐģŅĐ´ HDR-Đ˛Ņ–Đ´ĐĩĐž ĐŋŅ€Đ¸ ĐēĐžĐŊвĐĩŅ€Ņ‚Đ°Ņ†Ņ–Ņ— в SDR. КоĐļĐĩĐŊ аĐģĐŗĐžŅ€Đ¸Ņ‚Đŧ Ņ€ĐžĐąĐ¸Ņ‚ŅŒ Ņ€Ņ–ĐˇĐŊŅ– ĐēĐžĐŧĐŋŅ€ĐžĐŧŅ–ŅĐ¸ Ņ‰ĐžĐ´Đž ĐēĐžĐģŅŒĐžŅ€Ņƒ, Đ´ĐĩŅ‚Đ°ĐģŅ–ĐˇĐ°Ņ†Ņ–Ņ— Ņ‚Đ° ŅŅĐēŅ€Đ°Đ˛ĐžŅŅ‚Ņ–. АĐģĐŗĐžŅ€Đ¸Ņ‚Đŧ Hable СйĐĩŅ€Ņ–ĐŗĐ°Ņ” Đ´ĐĩŅ‚Đ°ĐģŅ–, Mobius - ĐēĐžĐģŅŒĐžŅ€Đ¸, Reinhard - ŅŅĐēŅ€Đ°Đ˛Ņ–ŅŅ‚ŅŒ.", - "transcoding_transcode_policy": "ПоĐģŅ–Ņ‚Đ¸Đēа ĐŋĐĩŅ€ĐĩĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ", - "transcoding_transcode_policy_description": "ПоĐģŅ–Ņ‚Đ¸Đēа Ņ‰ĐžĐ´Đž Ņ‚ĐžĐŗĐž, ĐēĐžĐģи Đ˛Ņ–Đ´ĐĩĐž ҁĐģŅ–Đ´ ĐŋĐĩŅ€ĐĩĐēĐžĐ´ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸. Đ’Ņ–Đ´ĐĩĐž С HDR Ņ– Đ˛Ņ–Đ´ĐĩĐž С ĐŋŅ–ĐēҁĐĩĐģҌĐŊиĐŧ Ņ„ĐžŅ€ĐŧĐ°Ņ‚ĐžĐŧ, Đ˛Ņ–Đ´ĐŧŅ–ĐŊĐŊиĐŧ Đ˛Ņ–Đ´ YUV 4:2:0, СавĐļди ĐąŅƒĐ´Đĩ ĐŋĐĩŅ€ĐĩĐēОдОваĐŊĐž (ĐēҀҖĐŧ виĐŋадĐēŅ–Đ˛, ĐēĐžĐģи ĐŋĐĩŅ€ĐĩĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ виĐŧĐēĐŊĐĩĐŊĐž).", + "transcoding_tone_mapping_description": "НаĐŧĐ°ĐŗĐ°Ņ”Ņ‚ŅŒŅŅ СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ Đ˛Đ¸ĐŗĐģŅĐ´ HDR-Đ˛Ņ–Đ´ĐĩĐž ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŋĐĩŅ€ĐĩŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŊа SDR. КоĐļĐĩĐŊ аĐģĐŗĐžŅ€Đ¸Ņ‚Đŧ Ņ€ĐžĐąĐ¸Ņ‚ŅŒ Ņ€Ņ–ĐˇĐŊŅ– ĐēĐžĐŧĐŋŅ€ĐžĐŧŅ–ŅĐ¸ Ņ‰ĐžĐ´Đž ĐēĐžĐģŅŒĐžŅ€Ņƒ, Đ´ĐĩŅ‚Đ°ĐģŅ–ĐˇĐ°Ņ†Ņ–Ņ— Ņ‚Đ° ŅŅĐēŅ€Đ°Đ˛ĐžŅŅ‚Ņ–. АĐģĐŗĐžŅ€Đ¸Ņ‚Đŧ Hable СйĐĩŅ€Ņ–ĐŗĐ°Ņ” Đ´ĐĩŅ‚Đ°ĐģŅ–, Mobius - ĐēĐžĐģŅŒĐžŅ€Đ¸, Reinhard - ŅŅĐēŅ€Đ°Đ˛Ņ–ŅŅ‚ŅŒ.", + "transcoding_transcode_policy": "ПоĐģŅ–Ņ‚Đ¸Đēа Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ", + "transcoding_transcode_policy_description": "ПоĐģŅ–Ņ‚Đ¸Đēа Ņ‰ĐžĐ´Đž Ņ‚ĐžĐŗĐž, ĐēĐžĐģи Đ˛Ņ–Đ´ĐĩĐž ҁĐģŅ–Đ´ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°Ņ‚Đ¸. Đ’Ņ–Đ´ĐĩĐž С HDR Ņ– Đ˛Ņ–Đ´ĐĩĐž С ĐŋŅ–ĐēҁĐĩĐģҌĐŊиĐŧ Ņ„ĐžŅ€ĐŧĐ°Ņ‚ĐžĐŧ, Đ˛Ņ–Đ´ĐŧŅ–ĐŊĐŊиĐŧ Đ˛Ņ–Đ´ YUV 4:2:0, СавĐļди ĐąŅƒĐ´Đĩ Ņ‚Ņ€Đ°ĐŊҁĐēОдОваĐŊĐž (ĐēҀҖĐŧ виĐŋадĐēŅ–Đ˛, ĐēĐžĐģи Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ виĐŧĐēĐŊĐĩĐŊĐž).", "transcoding_two_pass_encoding": "ĐšĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ С двОĐŧа ĐŋŅ€ĐžŅ…ĐžĐ´Đ°Đŧи", - "transcoding_two_pass_encoding_setting_description": "ĐĸŅ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ Са двОĐŧа ĐŋŅ€ĐžŅ…ĐžĐ´Đ°Đŧи Đ´ĐģŅ ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ ĐēŅ€Đ°Ņ‰Đ¸Ņ… СаĐēОдОваĐŊĐ¸Ņ… Đ˛Ņ–Đ´ĐĩĐž. КоĐģи Đ˛Đ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž ĐŧаĐēŅĐ¸ĐŧаĐģҌĐŊиК ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚ (ĐŊĐĩĐžĐąŅ…Ņ–Đ´ĐŊиК Đ´ĐģŅ Ņ€ĐžĐąĐžŅ‚Đ¸ С H.264 Ņ‚Đ° HEVC), ҆ĐĩĐš Ņ€ĐĩĐļиĐŧ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ” Đ´Ņ–Đ°ĐŋаСОĐŊ ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚Ņƒ, ĐˇĐ°ŅĐŊОваĐŊиК ĐŊа ĐŧаĐēŅĐ¸ĐŧаĐģҌĐŊĐžĐŧ҃ ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚Ņ–, Ņ– Ņ–ĐŗĐŊĐžŅ€ŅƒŅ” CRF. ДĐģŅ VP9 ĐŧĐžĐļĐŊа виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ CRF, ŅĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž ĐŧаĐēŅĐ¸ĐŧаĐģҌĐŊиК ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚.", + "transcoding_two_pass_encoding_setting_description": "ĐĸŅ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ Са двОĐŧа ĐŋŅ€ĐžŅ…ĐžĐ´Đ°Đŧи Đ´ĐģŅ ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ ĐēŅ€Đ°Ņ‰Đ¸Ņ… СаĐēОдОваĐŊĐ¸Ņ… Đ˛Ņ–Đ´ĐĩĐž. КоĐģи ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž ĐŧаĐēŅĐ¸ĐŧаĐģҌĐŊиК ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚ (ĐŊĐĩĐžĐąŅ…Ņ–Đ´ĐŊиК Đ´ĐģŅ Ņ€ĐžĐąĐžŅ‚Đ¸ С H.264 Ņ‚Đ° HEVC), ҆ĐĩĐš Ņ€ĐĩĐļиĐŧ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ” Đ´Ņ–Đ°ĐŋаСОĐŊ ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚Ņƒ, ĐŊа ĐžŅĐŊĐžĐ˛Ņ– ĐŧаĐēŅĐ¸ĐŧаĐģҌĐŊĐžĐŗĐž ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚Ņƒ, Ņ– Ņ–ĐŗĐŊĐžŅ€ŅƒŅ” CRF. ДĐģŅ VP9 ĐŧĐžĐļĐŊа виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ CRF, ŅĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž ĐŧаĐēŅĐ¸ĐŧаĐģҌĐŊиК ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚.", "transcoding_video_codec": "Đ’Ņ–Đ´ĐĩĐžĐēОдĐĩĐē", "transcoding_video_codec_description": "VP9 ĐŧĐ°Ņ” Đ˛Đ¸ŅĐžĐē҃ ĐĩŅ„ĐĩĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ Ņ– ҁ҃ĐŧҖҁĐŊŅ–ŅŅ‚ŅŒ С вĐĩйОĐŧ, аĐģĐĩ ĐŋĐžŅ‚Ņ€ĐĩĐąŅƒŅ” ĐąŅ–ĐģҌ҈Đĩ Ņ‡Đ°ŅŅƒ ĐŊа Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. HEVC ĐŋŅ€Đ°Ņ†ŅŽŅ” ŅŅ…ĐžĐļĐĩ, аĐģĐĩ ĐŧĐ°Ņ” ĐŧĐĩĐŊ҈҃ ҁ҃ĐŧҖҁĐŊŅ–ŅŅ‚ŅŒ С вĐĩйОĐŧ. H.264 ĐŧĐ°Ņ” ŅˆĐ¸Ņ€ĐžĐē҃ ҁ҃ĐŧҖҁĐŊŅ–ŅŅ‚ŅŒ Ņ– ŅˆĐ˛Đ¸Đ´ĐēĐž Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒŅ”Ņ‚ŅŒŅŅ, аĐģĐĩ ŅŅ‚Đ˛ĐžŅ€ŅŽŅ” СĐŊĐ°Ņ‡ĐŊĐž ĐąŅ–ĐģŅŒŅˆŅ– Ņ„Đ°ĐšĐģи. AV1 - ĐŊаКĐĩŅ„ĐĩĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅˆĐ¸Đš ĐēОдĐĩĐē, аĐģĐĩ ĐŊĐĩ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ”Ņ‚ŅŒŅŅ ĐŊа ŅŅ‚Đ°Ņ€Ņ–ŅˆĐ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŅ….", "trash_enabled_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐŊŅ ĐēĐžŅˆĐ¸Đēа", "trash_number_of_days": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ Đ´ĐŊŅ–Đ˛", - "trash_number_of_days_description": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ Đ´ĐŊŅ–Đ˛, ĐŋŅ€ĐžŅ‚ŅĐŗĐžĐŧ ŅĐēĐ¸Ņ… СаĐģĐ¸ŅˆĐ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ҃ ĐēĐžŅˆĐ¸Đē҃ ĐŋĐĩŅ€ĐĩĐ´ Ņ—Ņ… ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊиĐŧ видаĐģĐĩĐŊĐŊŅĐŧ", + "trash_number_of_days_description": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ Đ´ĐŊŅ–Đ˛ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ҃ ĐēĐžŅˆĐ¸Đē҃ ĐŋĐĩŅ€ĐĩĐ´ Ņ—Ņ… ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊиĐŧ видаĐģĐĩĐŊĐŊŅĐŧ", "trash_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐēĐžŅˆĐ¸Đēа", "trash_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐēĐžŅˆĐ¸Đēа", "unlink_all_oauth_accounts": "Đ’Ņ–Đ´â€™Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Đ˛ŅŅ– ОйĐģŅ–ĐēĐžĐ˛Ņ– СаĐŋĐ¸ŅĐ¸ OAuth", "unlink_all_oauth_accounts_description": "НĐĩ ĐˇĐ°ĐąŅƒĐ´ŅŒŅ‚Đĩ Đ˛Ņ–Đ´â€™Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Đ˛ŅŅ– ОйĐģŅ–ĐēĐžĐ˛Ņ– СаĐŋĐ¸ŅĐ¸ OAuth ĐŋĐĩŅ€ĐĩĐ´ ĐŋĐĩŅ€ĐĩŅ…ĐžĐ´ĐžĐŧ Đ´Đž ĐŊĐžĐ˛ĐžĐŗĐž ĐŋĐžŅŅ‚Đ°Ņ‡Đ°ĐģҌĐŊиĐēа.", "unlink_all_oauth_accounts_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Đ˛Ņ–Đ´â€™Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Đ˛ŅŅ– ОйĐģŅ–ĐēĐžĐ˛Ņ– СаĐŋĐ¸ŅĐ¸ OAuth? ĐĻĐĩ ҁĐēиĐŊĐĩ Ņ–Đ´ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ĐžŅ€ OAuth Đ´ĐģŅ ĐēĐžĐļĐŊĐžĐŗĐž ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°, Ņ– Ņ†ŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ĐąŅƒĐ´Đĩ ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸.", "user_cleanup_job": "ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "user_delete_delay": "ОбĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ {user} Ņ– ĐšĐžĐŗĐž Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ СаĐŋĐģаĐŊОваĐŊŅ– Đ´ĐģŅ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ ҇ĐĩŅ€ĐĩС {delay, plural, one {# Đ´ĐĩĐŊҌ} few {# Đ´ĐŊŅ–} many {# Đ´ĐŊŅ–Đ˛} other {# Đ´ĐŊŅ–Đ˛}}.", - "user_delete_delay_settings": "Đ’Ņ–Đ´ĐēĐģадĐĩĐŊĐĩ видаĐģĐĩĐŊĐŊŅ", - "user_delete_delay_settings_description": "ПĐĩŅ€Ņ–ĐžĐ´ Đ˛Ņ–Đ´Ņ‚ĐĩŅ€ĐŧŅ–ĐŊŅƒĐ˛Đ°ĐŊĐŊŅ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ ОйĐģŅ–ĐēĐžĐ˛ĐžĐŗĐž СаĐŋĐ¸ŅŅƒ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Ņ‚Đ° ĐšĐžĐŗĐž Ņ„Đ°ĐšĐģŅ–Đ˛. ЗавдаĐŊĐŊŅ С видаĐģĐĩĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° СаĐŋ҃ҁĐēĐ°Ņ”Ņ‚ŅŒŅŅ Ņ‰ĐžĐŊĐžŅ‡Ņ– Đž ĐŋŅ–Đ˛ĐŊĐžŅ‡Ņ– Ņ– ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€ŅŅ” ОйĐģŅ–ĐēĐžĐ˛Ņ– СаĐŋĐ¸ŅĐ¸, ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊŅ– Đ´ĐģŅ видаĐģĐĩĐŊĐŊŅ. ЗĐŧŅ–ĐŊи Ņ†ŅŒĐžĐŗĐž ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ° ĐąŅƒĐ´ŅƒŅ‚ŅŒ Đ˛Ņ€Đ°Ņ…ĐžĐ˛Đ°ĐŊŅ– ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊĐžĐŗĐž СаĐŋ҃ҁĐē҃ СавдаĐŊĐŊŅ.", - "user_delete_immediately": "ОбĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ Ņ‚Đ° Ņ„Đ°ĐšĐģи ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° {user} ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐŊĐĩĐŗĐ°ĐšĐŊĐž ĐŋĐžŅŅ‚Đ°Đ˛ĐģĐĩĐŊŅ– в ҇ĐĩŅ€ĐŗŅƒ ĐŊа ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐĩ видаĐģĐĩĐŊĐŊŅ.", - "user_delete_immediately_checkbox": "ĐŸĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Ņ‚Đ° Ņ„Đ°ĐšĐģи в ҇ĐĩŅ€ĐŗŅƒ Đ´ĐģŅ ĐŊĐĩĐŗĐ°ĐšĐŊĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ", + "user_delete_delay": "ОбĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ {user} Ņ‚Đ° ĐšĐžĐŗĐž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐąŅƒĐ´Đĩ СаĐŋĐģаĐŊОваĐŊĐž Đ´ĐģŅ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ ҇ĐĩŅ€ĐĩС {delay, plural, one {# Đ´ĐĩĐŊҌ} few {# Đ´ĐŊŅ–} many {# Đ´ĐŊŅ–Đ˛} other {# Đ´ĐŊŅ–Đ˛}}.", + "user_delete_delay_settings": "Đ—Đ°Ņ‚Ņ€Đ¸ĐŧĐēа видаĐģĐĩĐŊĐŊŅ", + "user_delete_delay_settings_description": "ПĐĩŅ€Ņ–ĐžĐ´ Đ˛Ņ–Đ´Ņ‚ĐĩŅ€ĐŧŅ–ĐŊŅƒĐ˛Đ°ĐŊĐŊŅ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ ОйĐģŅ–ĐēĐžĐ˛ĐžĐŗĐž СаĐŋĐ¸ŅŅƒ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Ņ‚Đ° ĐšĐžĐŗĐž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛. ЗавдаĐŊĐŊŅ С видаĐģĐĩĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° виĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ Ņ‰ĐžĐŊĐžŅ‡Ņ– Đž ĐŋŅ–Đ˛ĐŊĐžŅ‡Ņ– Ņ– ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€ŅŅ” ОйĐģŅ–ĐēĐžĐ˛Ņ– СаĐŋĐ¸ŅĐ¸, ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊŅ– Đ´ĐģŅ видаĐģĐĩĐŊĐŊŅ. ЗĐŧŅ–ĐŊи Ņ†ŅŒĐžĐŗĐž ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ° ĐąŅƒĐ´Đĩ Đ˛Ņ€Đ°Ņ…ĐžĐ˛Đ°ĐŊĐž ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊĐžĐŗĐž виĐēĐžĐŊаĐŊĐŊŅ СавдаĐŊĐŊŅ.", + "user_delete_immediately": "ОбĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ Ņ‚Đ° ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° {user} ĐąŅƒĐ´Đĩ ĐŊĐĩĐŗĐ°ĐšĐŊĐž ĐŋĐžŅŅ‚Đ°Đ˛ĐģĐĩĐŊĐž в ҇ĐĩŅ€ĐŗŅƒ ĐŊа ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐĩ видаĐģĐĩĐŊĐŊŅ.", + "user_delete_immediately_checkbox": "ĐŸĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Ņ‚Đ° ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ в ҇ĐĩŅ€ĐŗŅƒ Đ´ĐģŅ ĐŊĐĩĐŗĐ°ĐšĐŊĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ", "user_details": "ДаĐŊŅ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "user_management": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧи", "user_password_has_been_reset": "ĐŸĐ°Ņ€ĐžĐģҌ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐąŅƒĐģĐž ҁĐēиĐŊŅƒŅ‚Đž:", - "user_password_reset_description": "Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐŊĐ°Đ´Đ°ĐšŅ‚Đĩ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ĐĩĐ˛Ņ– Ņ‚Đ¸ĐŧŅ‡Đ°ŅĐžĐ˛Đ¸Đš ĐŋĐ°Ņ€ĐžĐģҌ Ņ– ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧŅ‚Đĩ КОĐŧ҃, Ņ‰Đž Đ˛Ņ–ĐŊ ĐŋОвиĐŊĐĩĐŊ ĐąŅƒĐ´Đĩ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ ĐŋŅ€Đ¸ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊĐžĐŧ҃ Đ˛Ņ…ĐžĐ´Ņ–.", + "user_password_reset_description": "Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐŊĐ°Đ´Đ°ĐšŅ‚Đĩ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ĐĩĐ˛Ņ– Ņ‚Đ¸ĐŧŅ‡Đ°ŅĐžĐ˛Đ¸Đš ĐŋĐ°Ņ€ĐžĐģҌ Ņ– ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧŅ‚Đĩ, Ņ‰Đž ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž ĐąŅƒĐ´Đĩ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐšĐžĐŗĐž ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊĐžĐŗĐž Đ˛Ņ…ĐžĐ´Ņƒ.", "user_restore_description": "ОбĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ {user} ĐąŅƒĐ´Đĩ Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž.", "user_restore_scheduled_removal": "Đ’Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° - СаĐŋĐģаĐŊОваĐŊĐž ĐŊа видаĐģĐĩĐŊĐŊŅ {date, date, long}", "user_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "user_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛", - "user_successfully_removed": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° {email} ҃ҁĐŋŅ–ŅˆĐŊĐž видаĐģĐĩĐŊĐž.", - "users_page_description": "ĐĄŅ‚ĐžŅ€Ņ–ĐŊĐēа адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Ņ–Đ˛", - "version_check_enabled_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đē҃ вĐĩҀҁҖҗ", - "version_check_implications": "Đ¤ŅƒĐŊĐēŅ†Ņ–Ņ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēи вĐĩҀҁҖҗ СаĐģĐĩĐļĐ¸Ņ‚ŅŒ Đ˛Ņ–Đ´ ĐŋĐĩŅ€Ņ–ĐžĐ´Đ¸Ņ‡ĐŊĐžŅ— ĐēĐžĐŧ҃ĐŊŅ–ĐēĐ°Ņ†Ņ–Ņ— С github.com", + "user_successfully_removed": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° {email} виĐģŅƒŅ‡ĐĩĐŊĐž.", + "users_page_description": "ĐĄŅ‚ĐžŅ€Ņ–ĐŊĐēа ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", + "version_check_enabled_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐŊŅ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēи вĐĩҀҁҖҗ", + "version_check_implications": "Đ¤ŅƒĐŊĐēŅ†Ņ–Ņ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēи вĐĩҀҁҖҗ СаĐģĐĩĐļĐ¸Ņ‚ŅŒ Đ˛Ņ–Đ´ ĐŋĐĩŅ€Ņ–ĐžĐ´Đ¸Ņ‡ĐŊĐžŅ— ĐēĐžĐŧ҃ĐŊŅ–ĐēĐ°Ņ†Ņ–Ņ— С {server}", "version_check_settings": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēа вĐĩҀҁҖҗ", "version_check_settings_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸/виĐŧĐēĐŊŅƒŅ‚Đ¸ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ ĐŋŅ€Đž ĐŊĐžĐ˛Ņƒ вĐĩŅ€ŅŅ–ŅŽ", - "video_conversion_job": "ПĐĩŅ€ĐĩĐēĐžĐ´ŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛Ņ–Đ´ĐĩĐž", + "video_conversion_job": "ĐĸŅ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛Ņ–Đ´ĐĩĐž", "video_conversion_job_description": "ĐĸŅ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛Ņ–Đ´ĐĩĐž Đ´ĐģŅ ŅˆĐ¸Ņ€ŅˆĐžŅ— ҁ҃ĐŧҖҁĐŊĐžŅŅ‚Ņ– С ĐąŅ€Đ°ŅƒĐˇĐĩŅ€Đ°Đŧи Ņ‚Đ° ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅĐŧи" }, "admin_email": "ЕĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊа ĐŋĐžŅˆŅ‚Đ° адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", @@ -453,84 +453,84 @@ "advanced": "Đ ĐžĐˇŅˆĐ¸Ņ€ĐĩĐŊŅ–", "advanced_settings_clear_image_cache": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐĩ҈ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ", "advanced_settings_clear_image_cache_error": "НĐĩ вдаĐģĐžŅŅ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐĩ҈ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ", - "advanced_settings_clear_image_cache_success": "ĐŖŅĐŋŅ–ŅˆĐŊĐž ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐž {size}", - "advanced_settings_enable_alternate_media_filter_subtitle": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ ҆ĐĩĐš Đ˛Đ°Ņ€Ņ–Đ°ĐŊŅ‚ Đ´ĐģŅ ҄ҖĐģŅŒŅ‚Ņ€Đ°Ņ†Ņ–Ņ— Ņ„Đ°ĐšĐģŅ–Đ˛ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ— Са аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛ĐŊиĐŧи ĐēŅ€Đ¸Ņ‚ĐĩŅ€Ņ–ŅĐŧи. ĐĄĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ ҆Đĩ, ŅĐēŅ‰Đž ҃ Đ˛Đ°Ņ виĐŊиĐēĐ°ŅŽŅ‚ŅŒ ĐŋŅ€ĐžĐąĐģĐĩĐŧи С Ņ‚Đ¸Đŧ, Ņ‰Đž ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē ĐŊĐĩ Đ˛Đ¸ŅĐ˛ĐģŅŅ” Đ˛ŅŅ– аĐģŅŒĐąĐžĐŧи.", - "advanced_settings_enable_alternate_media_filter_title": "[ЕКСПЕРИМЕНĐĸАЛĐŦНИЙ] ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛ĐŊиК ҄ҖĐģŅŒŅ‚Ņ€ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ— аĐģŅŒĐąĐžĐŧŅ–Đ˛ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", + "advanced_settings_clear_image_cache_success": "ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐž {size}", + "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_prefer_remote_subtitle": "ДĐĩŅĐēŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— Đ´ŅƒĐļĐĩ ĐŋĐžĐ˛Ņ–ĐģҌĐŊĐž СаваĐŊŅ‚Đ°ĐļŅƒŅŽŅ‚ŅŒ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ С ĐģĐžĐēаĐģҌĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛. ĐŖĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ ҆ĐĩĐš ĐŋĐ°Ņ€Đ°ĐŧĐĩ҂Ҁ, Ņ‰ĐžĐą ĐŊĐ°Ņ‚ĐžĐŧŅ–ŅŅ‚ŅŒ СаваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊŅ– ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ.", + "advanced_settings_prefer_remote_title": "ĐĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊиĐŧ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅĐŧ", "advanced_settings_proxy_headers_subtitle": "ВизĐŊĐ°Ņ‡Ņ‚Đĩ ĐˇĐ°ĐŗĐžĐģОвĐēи ĐŋŅ€ĐžĐēҁҖ-ҁĐĩŅ€Đ˛ĐĩŅ€Đ°, ŅĐēŅ– Immich ĐŧĐ°Ņ” ĐŊĐ°Đ´ŅĐ¸ĐģĐ°Ņ‚Đ¸ С ĐēĐžĐļĐŊиĐŧ ĐŧĐĩŅ€ĐĩĐļĐĩвиĐŧ СаĐŋĐ¸Ņ‚ĐžĐŧ", - "advanced_settings_proxy_headers_title": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ†ŅŒĐēŅ– ĐŋŅ€ĐžĐēҁҖ-ĐˇĐ°ĐŗĐžĐģОвĐēи [ЕКСПЕРИМЕНĐĸАЛĐŦНА Đ’Đ•Đ ĐĄĐ†Đ¯]", - "advanced_settings_readonly_mode_subtitle": "ĐŖĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐŊŅ Ņ€ĐĩĐļиĐŧ҃ ҂ҖĐģҌĐēи Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ, в ŅĐēĐžĐŧ҃ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— ĐŧĐžĐļĐŊа ҂ҖĐģҌĐēи ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‚Đ¸, а Ņ‚Đ°ĐēŅ– Ņ„ŅƒĐŊĐē҆Җҗ, ŅĐē Đ˛Đ¸ĐąŅ–Ņ€ Đ´ĐĩĐēŅ–ĐģҌĐēĐžŅ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ, ҁĐŋŅ–ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ, ĐŋĐĩŅ€ĐĩĐ´Đ°Ņ‡Đ°, видаĐģĐĩĐŊĐŊŅ, виĐŧĐēĐŊĐĩĐŊŅ–. ĐŖĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐŊŅ/виĐŧĐēĐŊĐĩĐŊĐŊŅ Ņ€ĐĩĐļиĐŧ҃ ҂ҖĐģҌĐēи Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ Đ°Đ˛Đ°Ņ‚Đ°Ņ€Đ° ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐŊа ĐŗĐžĐģОвĐŊĐžĐŧ҃ ĐĩĐēŅ€Đ°ĐŊŅ–", + "advanced_settings_proxy_headers_title": "Đ”ĐžĐ˛Ņ–ĐģҌĐŊŅ– ĐŋŅ€ĐžĐēҁҖ-ĐˇĐ°ĐŗĐžĐģОвĐēи [ЕКСПЕРИМЕНĐĸАЛĐŦНО]", + "advanced_settings_readonly_mode_subtitle": "ĐŖĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐŊŅ Ņ€ĐĩĐļиĐŧ҃ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ, ҃ ŅĐēĐžĐŧ҃ Ņ„ĐžŅ‚Đž ĐŧĐžĐļĐŊа ĐģĐ¸ŅˆĐĩ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‚Đ¸, а Ņ‚Đ°ĐēŅ– Ņ„ŅƒĐŊĐē҆Җҗ, ŅĐē Đ˛Đ¸ĐąŅ–Ņ€ ĐēŅ–ĐģҌĐēĐžŅ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ, ҁĐŋŅ–ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ, Ņ‚Ņ€Đ°ĐŊҁĐģŅŅ†Ņ–Ņ, видаĐģĐĩĐŊĐŊŅ — виĐŧĐēĐŊĐĩĐŊĐž. ĐŖĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐŊŅ/виĐŧĐēĐŊĐĩĐŊĐŊŅ Ņ€ĐĩĐļиĐŧ҃ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ Đ°Đ˛Đ°Ņ‚Đ°Ņ€Đ° ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐŊа ĐŗĐžĐģОвĐŊĐžĐŧ҃ ĐĩĐēŅ€Đ°ĐŊŅ–", "advanced_settings_readonly_mode_title": "Đ ĐĩĐļиĐŧ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ", "advanced_settings_self_signed_ssl_subtitle": "ĐŸŅ€ĐžĐŋ҃ҁĐēĐ°Ņ” ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đē҃ SSL-ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ° ҁĐĩŅ€Đ˛ĐĩŅ€Đ°. ĐŸĐžŅ‚Ņ€Ņ–ĐąĐŊĐĩ Đ´ĐģŅ ŅĐ°ĐŧĐžĐŋŅ–Đ´ĐŋĐ¸ŅĐ°ĐŊĐ¸Ņ… ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Ņ–Đ˛.", - "advanced_settings_self_signed_ssl_title": "ДозвоĐģĐ¸Ņ‚Đ¸ ŅĐ°ĐŧĐžĐŋŅ–Đ´ĐŋĐ¸ŅĐ°ĐŊŅ– SSL-ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ¸ [ЕКСПЕРИМЕНĐĸАЛĐŦНА Đ’Đ•Đ ĐĄĐ†Đ¯]", - "advanced_settings_sync_remote_deletions_subtitle": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž видаĐģŅŅ‚Đ¸ айО Đ˛Ņ–Đ´ĐŊОвĐģŅŽĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģ ĐŊа Ņ†ŅŒĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—, ĐēĐžĐģи Ņ†Ņ Đ´Ņ–Ņ виĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ в вĐĩĐą-Ņ–ĐŊŅ‚ĐĩҀ҄ĐĩĐšŅŅ–", + "advanced_settings_self_signed_ssl_title": "ХаĐŧĐžĐŋŅ–Đ´ĐŋĐ¸ŅĐ°ĐŊŅ– SSL-ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ¸ [ЕКСПЕРИМЕНĐĸАЛĐŦНО]", + "advanced_settings_sync_remote_deletions_subtitle": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž видаĐģŅŅ‚Đ¸ айО Đ˛Ņ–Đ´ĐŊОвĐģŅŽĐ˛Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ ĐŊа Ņ†ŅŒĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—, ĐēĐžĐģи Ņ†Ņ Đ´Ņ–Ņ виĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ ҃ вĐĩĐą-Ņ–ĐŊŅ‚ĐĩҀ҄ĐĩĐšŅŅ–", "advanced_settings_sync_remote_deletions_title": "ХиĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊĐ¸Ņ… видаĐģĐĩĐŊҌ [ЕКСПЕРИМЕНĐĸАЛĐŦНО]", - "advanced_settings_tile_subtitle": "Đ ĐžĐˇŅˆĐ¸Ņ€ĐĩĐŊŅ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ†ŅŒĐēŅ– ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", - "advanced_settings_troubleshooting_subtitle": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Ņ– Ņ„ŅƒĐŊĐē҆Җҗ Đ´ĐģŅ ҃ҁ҃ĐŊĐĩĐŊĐŊŅ ĐŊĐĩҁĐŋŅ€Đ°Đ˛ĐŊĐžŅŅ‚ĐĩĐš", + "advanced_settings_tile_subtitle": "Đ ĐžĐˇŅˆĐ¸Ņ€ĐĩĐŊŅ– ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", + "advanced_settings_troubleshooting_subtitle": "ĐŖĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐŊŅ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Đ¸Ņ… Ņ„ŅƒĐŊĐēŅ†Ņ–Đš Đ´ĐģŅ ҃ҁ҃ĐŊĐĩĐŊĐŊŅ ĐŊĐĩҁĐŋŅ€Đ°Đ˛ĐŊĐžŅŅ‚ĐĩĐš", "advanced_settings_troubleshooting_title": "ĐŖŅŅƒĐŊĐĩĐŊĐŊŅ ĐŊĐĩҁĐŋŅ€Đ°Đ˛ĐŊĐžŅŅ‚ĐĩĐš", "age_months": "Đ’Ņ–Đē {months, plural, one {# ĐŧŅ–ŅŅŅ†ŅŒ} few {# ĐŧŅ–ŅŅŅ†Ņ–} many {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛} other {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛}}", "age_year_months": "Đ’Ņ–Đē 1 ҀҖĐē, {months, plural, one {# ĐŧŅ–ŅŅŅ†ŅŒ} few {# ĐŧŅ–ŅŅŅ†Ņ–} many {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛} other {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛}}", - "age_years": "{years, plural, other {Đ’Ņ–Đē #}}", + "age_years": "{years, plural, one {Đ’Ņ–Đē #} few {Đ’Ņ–Đē #} many {Đ’Ņ–Đē #} other {Đ’Ņ–Đē #}}", "album": "АĐģŅŒĐąĐžĐŧ", "album_added": "АĐģŅŒĐąĐžĐŧ дОдаĐŊĐž", "album_added_notification_setting_description": "ĐžŅ‚Ņ€Đ¸ĐŧŅƒĐ˛Đ°Ņ‚Đ¸ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅŽ ĐŋĐžŅˆŅ‚ĐžŅŽ, ĐēĐžĐģи Đ˛Đ°Ņ Đ´ĐžĐ´Đ°ŅŽŅ‚ŅŒ Đ´Đž ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž аĐģŅŒĐąĐžĐŧ҃", - "album_cover_updated": "ОбĐēĐģадиĐŊĐēа аĐģŅŒĐąĐžĐŧ҃ ĐžĐŊОвĐģĐĩĐŊа", + "album_cover_updated": "ОбĐēĐģадиĐŊĐē҃ аĐģŅŒĐąĐžĐŧ҃ ĐžĐŊОвĐģĐĩĐŊĐž", "album_delete_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ {album}?", - "album_delete_confirmation_description": "Đ¯ĐēŅ‰Đž аĐģŅŒĐąĐžĐŧ ĐąŅƒĐ˛ ҁĐŋŅ–ĐģҌĐŊиĐŧ, Ņ–ĐŊŅˆŅ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ– ĐŊĐĩ СĐŧĐžĐļŅƒŅ‚ŅŒ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Đ´ĐžŅŅ‚ŅƒĐŋ Đ´Đž ĐŊŅŒĐžĐŗĐž.", + "album_delete_confirmation_description": "Đ¯ĐēŅ‰Đž аĐģŅŒĐąĐžĐŧ Ņ” ҁĐŋŅ–ĐģҌĐŊиĐŧ, Ņ–ĐŊŅˆŅ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ– ĐąŅ–ĐģҌ҈Đĩ ĐŊĐĩ СĐŧĐžĐļŅƒŅ‚ŅŒ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Đ´ĐžŅŅ‚ŅƒĐŋ Đ´Đž ĐŊŅŒĐžĐŗĐž.", "album_deleted": "АĐģŅŒĐąĐžĐŧ видаĐģĐĩĐŊĐž", - "album_info_card_backup_album_excluded": "Đ’Đ˜Đ›ĐŖĐ§Đ•ĐĐ˜Đ™", - "album_info_card_backup_album_included": "ВКЛЮЧЕНИЙ", - "album_info_updated": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž аĐģŅŒĐąĐžĐŧ ĐžĐŊОвĐģĐĩĐŊа", - "album_leave": "ЗаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ?", - "album_leave_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ СаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ {album}?", - "album_name": "Назва АĐģŅŒĐąĐžĐŧ҃", - "album_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ аĐģŅŒĐąĐžĐŧ҃", - "album_remove_user": "ВидаĐģĐ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°?", - "album_remove_user_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ {user}?", + "album_info_card_backup_album_excluded": "НЕ ВРАĐĨĐžĐ’ĐŖĐ„ĐĸĐŦĐĄĐ¯", + "album_info_card_backup_album_included": "ВРАĐĨĐžĐ’ĐŖĐ„ĐĸĐŦĐĄĐ¯", + "album_info_updated": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–ŅŽ ĐŋŅ€Đž аĐģŅŒĐąĐžĐŧ ĐžĐŊОвĐģĐĩĐŊĐž", + "album_leave": "ПоĐēиĐŊŅƒŅ‚Đ¸ аĐģŅŒĐąĐžĐŧ?", + "album_leave_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŋĐžĐēиĐŊŅƒŅ‚Đ¸ аĐģŅŒĐąĐžĐŧ {album}?", + "album_name": "Назва аĐģŅŒĐąĐžĐŧ҃", + "album_options": "Đ’Đ°Ņ€Ņ–Đ°ĐŊŅ‚Đ¸ аĐģŅŒĐąĐžĐŧ҃", + "album_remove_user": "ВиĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°?", + "album_remove_user_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ {user}?", "album_search_not_found": "АĐģŅŒĐąĐžĐŧŅ–Đ˛, Ņ‰Đž Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°ŅŽŅ‚ŅŒ Đ˛Đ°ŅˆĐžĐŧ҃ СаĐŋĐ¸Ņ‚Ņƒ, ĐŊĐĩ СĐŊаКдĐĩĐŊĐž", "album_selected": "АĐģŅŒĐąĐžĐŧ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž", - "album_share_no_users": "ĐĄŅ…ĐžĐļĐĩ, ви ĐŋĐžĐ´Ņ–ĐģиĐģĐ¸ŅŅ Ņ†Đ¸Đŧ аĐģŅŒĐąĐžĐŧĐžĐŧ С ŅƒŅŅ–Đŧа ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧи айО ҃ Đ˛Đ°Ņ ĐŊĐĩĐŧĐ°Ņ” ĐļОдĐŊĐžĐŗĐž ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°, С ŅĐēиĐŧ ĐŧĐžĐļĐŊа ĐąŅƒĐģĐž Đą ĐŋĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ.", - "album_summary": "ĐšĐžŅ€ĐžŅ‚ĐēиК ĐžĐŋĐ¸Ņ аĐģŅŒĐąĐžĐŧ҃", + "album_share_no_users": "ĐĄŅ…ĐžĐļĐĩ, ҆ĐĩĐš аĐģŅŒĐąĐžĐŧ вĐļĐĩ Đ´ĐžŅŅ‚ŅƒĐŋĐŊиК ŅƒŅŅ–Đŧ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧ, айО ĐŊĐĩĐŧĐ°Ņ” ĐēĐžĐŗĐž Đ´ĐžĐ´Đ°Ņ‚Đ¸.", + "album_summary": "ЗвĐĩĐ´ĐĩĐŊĐŊŅ аĐģŅŒĐąĐžĐŧ҃", "album_updated": "АĐģŅŒĐąĐžĐŧ ĐžĐŊОвĐģĐĩĐŊĐž", - "album_updated_setting_description": "ĐžŅ‚Ņ€Đ¸ĐŧŅƒĐšŅ‚Đĩ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ ĐŊа ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊ҃ ĐŋĐžŅˆŅ‚Ņƒ, ĐēĐžĐģи ҃ ҁĐŋŅ–ĐģҌĐŊĐžĐŧ҃ аĐģŅŒĐąĐžĐŧŅ– С'ŅĐ˛ĐģŅŅŽŅ‚ŅŒŅŅ ĐŊĐžĐ˛Ņ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", - "album_upload_assets": "ВиваĐŊŅ‚Đ°ĐļŅ‚Đĩ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ĐˇŅ– ŅĐ˛ĐžĐŗĐž ĐēĐžĐŧĐŋ'ŅŽŅ‚ĐĩŅ€Đ° Ņ‚Đ° Đ´ĐžĐ´Đ°ĐšŅ‚Đĩ Ņ—Ņ… Đ´Đž аĐģŅŒĐąĐžĐŧ҃", + "album_updated_setting_description": "ĐžŅ‚Ņ€Đ¸ĐŧŅƒĐ˛Đ°Ņ‚Đ¸ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅŽ ĐŋĐžŅˆŅ‚ĐžŅŽ, ĐēĐžĐģи ҃ ҁĐŋŅ–ĐģҌĐŊĐžĐŧ҃ аĐģŅŒĐąĐžĐŧŅ– С'ŅĐ˛ĐģŅŅŽŅ‚ŅŒŅŅ ĐŊĐžĐ˛Ņ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", + "album_upload_assets": "ВиваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐˇŅ– ŅĐ˛ĐžĐŗĐž ĐēĐžĐŧĐŋ'ŅŽŅ‚ĐĩŅ€Đ° Ņ‚Đ° Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", "album_user_left": "Ви ĐŋĐžĐēиĐŊ҃Đģи {album}", - "album_user_removed": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ {user} видаĐģĐĩĐŊиК", + "album_user_removed": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° {user} виĐģŅƒŅ‡ĐĩĐŊĐž", "album_viewer_appbar_delete_confirm": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ҆ĐĩĐš аĐģŅŒĐąĐžĐŧ ĐˇŅ– ŅĐ˛ĐžĐŗĐž ОйĐģŅ–ĐēĐžĐ˛ĐžĐŗĐž СаĐŋĐ¸ŅŅƒ?", "album_viewer_appbar_share_err_delete": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", "album_viewer_appbar_share_err_leave": "НĐĩ вдаĐģĐžŅŅ Đ˛Đ¸ĐšŅ‚Đ¸ С аĐģŅŒĐąĐžĐŧ҃", - "album_viewer_appbar_share_err_remove": "ВиĐŊиĐēĐģи ĐŋŅ€ĐžĐąĐģĐĩĐŧи С видаĐģĐĩĐŊĐŊŅĐŧ Ņ„Đ°ĐšĐģŅ–Đ˛ С аĐģŅŒĐąĐžĐŧ҃", + "album_viewer_appbar_share_err_remove": "НĐĩ вдаĐģĐžŅŅ виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ С аĐģŅŒĐąĐžĐŧ҃", "album_viewer_appbar_share_err_title": "НĐĩ вдаĐģĐžŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŊĐ°ĐˇĐ˛Ņƒ аĐģŅŒĐąĐžĐŧ҃", "album_viewer_appbar_share_leave": "Đ’Đ¸ĐšŅ‚Đ¸ С аĐģŅŒĐąĐžĐŧ҃", "album_viewer_appbar_share_to": "ĐŸĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ", "album_viewer_page_share_add_users": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛", - "album_with_link_access": "Đ‘ŅƒĐ´ŅŒ-Ņ…Ņ‚Đž С ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧ ĐŧĐžĐļĐĩ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž в Ņ†ŅŒĐžĐŧ҃ аĐģŅŒĐąĐžĐŧŅ–.", + "album_with_link_access": "Đ‘ŅƒĐ´ŅŒ-Ņ…Ņ‚Đž С ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧ ĐŧĐžĐļĐĩ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž Ņ‚Đ° ĐģŅŽĐ´ĐĩĐš ҃ Ņ†ŅŒĐžĐŧ҃ аĐģŅŒĐąĐžĐŧŅ–.", "albums": "АĐģŅŒĐąĐžĐŧи", - "albums_count": "{count, plural, one {1 аĐģŅŒĐąĐžĐŧ} few {{count, number} аĐģŅŒĐąĐžĐŧи} many {{count, number} аĐģŅŒĐąĐžĐŧŅ–Đ˛} other {{count, number} аĐģŅŒĐąĐžĐŧŅ–Đ˛}}", - "albums_default_sort_order": "ĐŸĐžŅ€ŅĐ´ĐžĐē ŅĐžŅ€Ņ‚ŅƒĐ˛Đ°ĐŊĐŊŅ аĐģŅŒĐąĐžĐŧŅ–Đ˛ Са СаĐŧĐžĐ˛Ņ‡ŅƒĐ˛Đ°ĐŊŅĐŧ", - "albums_default_sort_order_description": "ĐŸĐžŅ‡Đ°Ņ‚ĐēОвиК ĐŋĐžŅ€ŅĐ´ĐžĐē ŅĐžŅ€Ņ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ„Đ°ĐšĐģŅ–Đ˛ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŊĐžĐ˛Đ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛.", - "albums_feature_description": "КоĐģĐĩĐē҆Җҗ Ņ„Đ°ĐšĐģŅ–Đ˛, ŅĐēŅ– ĐŧĐžĐļĐŊа ҁĐŋŅ–ĐģҌĐŊĐž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ С Ņ–ĐŊŅˆĐ¸Đŧи ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧи.", + "albums_count": "{count, plural, one {{count, number} аĐģŅŒĐąĐžĐŧ} few {{count, number} аĐģŅŒĐąĐžĐŧи} many {{count, number} аĐģŅŒĐąĐžĐŧŅ–Đ˛} other {{count, number} аĐģŅŒĐąĐžĐŧŅ–Đ˛}}", + "albums_default_sort_order": "ĐĸиĐŋОвиК ĐŋĐžŅ€ŅĐ´ĐžĐē ŅĐžŅ€Ņ‚ŅƒĐ˛Đ°ĐŊĐŊŅ аĐģŅŒĐąĐžĐŧŅ–Đ˛", + "albums_default_sort_order_description": "ĐŸĐžŅ‡Đ°Ņ‚ĐēОвиК ĐŋĐžŅ€ŅĐ´ĐžĐē ŅĐžŅ€Ņ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŊĐžĐ˛Đ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛.", + "albums_feature_description": "КоĐģĐĩĐē҆Җҗ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛, ŅĐēиĐŧи ĐŧĐžĐļĐŊа Đ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ С Ņ–ĐŊŅˆĐ¸Đŧи ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧи.", "albums_on_device_count": "АĐģŅŒĐąĐžĐŧи ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— ({count})", "albums_selected": "{count, plural, one {# аĐģŅŒĐąĐžĐŧ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž} few {# аĐģŅŒĐąĐžĐŧи Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž} many {# аĐģŅŒĐąĐžĐŧŅ–Đ˛ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž} other {# аĐģŅŒĐąĐžĐŧŅ–Đ˛ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž}}", "all": "ĐŖŅŅ–", "all_albums": "ĐŖŅŅ– аĐģŅŒĐąĐžĐŧи", "all_people": "ĐŖŅŅ– ĐģŅŽĐ´Đ¸", - "all_photos": "ĐŖŅŅ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—", + "all_photos": "ĐŖŅŅ– Ņ„ĐžŅ‚Đž", "all_videos": "ĐŖŅŅ– Đ˛Ņ–Đ´ĐĩĐž", - "allow_dark_mode": "ДозвоĐģĐ¸Ņ‚Đ¸ Ņ‚ĐĩĐŧĐŊиК Ņ€ĐĩĐļиĐŧ", - "allow_edits": "ДозвоĐģĐ¸Ņ‚Đ¸ Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°ĐŊĐŊŅ", - "allow_public_user_to_download": "ДозвоĐģĐ¸Ņ‚Đ¸ ĐŋŅƒĐąĐģҖ҇ĐŊĐžĐŧ҃ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ĐĩĐ˛Ņ– СаваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", - "allow_public_user_to_upload": "ДозвоĐģĐ¸Ņ‚Đ¸ ĐŋŅƒĐąĐģҖ҇ĐŊиĐŧ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧ виваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸", + "allow_dark_mode": "ĐĸĐĩĐŧĐŊа Ņ‚ĐĩĐŧа ĐžŅ„ĐžŅ€ĐŧĐģĐĩĐŊĐŊŅ", + "allow_edits": "Đ”Đ°Ņ‚Đ¸ СĐŧĐžĐŗŅƒ Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸", + "allow_public_user_to_download": "Đ”Đ°Ņ‚Đ¸ СĐŧĐžĐŗŅƒ ĐŋŅƒĐąĐģҖ҇ĐŊĐžĐŧ҃ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ĐĩĐ˛Ņ– СаваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", + "allow_public_user_to_upload": "Đ”Đ°Ņ‚Đ¸ СĐŧĐžĐŗŅƒ ĐŋŅƒĐąĐģҖ҇ĐŊĐžĐŧ҃ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ĐĩĐ˛Ņ– виваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", "allowed": "ДозвоĐģĐĩĐŊĐž", "alt_text_qr_code": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ QR-ĐēĐžĐ´Ņƒ", "always_keep": "ЗавĐļди СйĐĩŅ€Ņ–ĐŗĐ°Ņ‚Đ¸", - "always_keep_photos_hint": "Đ¤ŅƒĐŊĐēŅ†Ņ–Ņ ÂĢĐ—Đ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆ĐĩÂģ СйĐĩŅ€ĐĩĐļĐĩ Đ˛ŅŅ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— ĐŊа Ņ†ŅŒĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—.", + "always_keep_photos_hint": "Đ¤ŅƒĐŊĐēŅ†Ņ–Ņ ÂĢĐ—Đ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆ĐĩÂģ СйĐĩŅ€ĐĩĐļĐĩ Đ˛ŅŅ– Ņ„ĐžŅ‚Đž ĐŊа Ņ†ŅŒĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—.", "always_keep_videos_hint": "Đ¤ŅƒĐŊĐēŅ†Ņ–Ņ ÂĢĐ—Đ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆ĐĩÂģ СйĐĩŅ€ĐĩĐļĐĩ Đ˛ŅŅ– Đ˛Ņ–Đ´ĐĩĐž ĐŊа Ņ†ŅŒĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—.", "anti_clockwise": "ĐŸŅ€ĐžŅ‚Đ¸ ĐŗĐžĐ´Đ¸ĐŊĐŊиĐēĐžĐ˛ĐžŅ— ҁ҂ҀҖĐģĐēи", "api_key": "КĐģŅŽŅ‡ API", - "api_key_description": "ĐĻĐĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐąŅƒĐ´Đĩ ĐŋĐžĐēаСаĐŊĐĩ ĐģĐ¸ŅˆĐĩ ОдиĐŊ Ņ€Đ°Đˇ. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ОйОв'ŅĐˇĐēОвО ҁĐēĐžĐŋŅ–ŅŽĐšŅ‚Đĩ ĐšĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐ´ СаĐēŅ€Đ¸Ņ‚Ņ‚ŅĐŧ Đ˛Ņ–ĐēĐŊа.", + "api_key_description": "ĐĻĐĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐąŅƒĐ´Đĩ ĐŋĐžĐēаСаĐŊĐž ĐģĐ¸ŅˆĐĩ ОдиĐŊ Ņ€Đ°Đˇ. Обов'ŅĐˇĐēОвО ҁĐēĐžĐŋŅ–ŅŽĐšŅ‚Đĩ ĐšĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐ´ СаĐēŅ€Đ¸Ņ‚Ņ‚ŅĐŧ Đ˛Ņ–ĐēĐŊа.", "api_key_empty": "Назва Đ˛Đ°ŅˆĐžĐŗĐž ĐēĐģŅŽŅ‡Đ° API ĐŊĐĩ ĐŧĐžĐļĐĩ ĐąŅƒŅ‚Đ¸ ĐŋĐžŅ€ĐžĐļĐŊŅŒĐžŅŽ", "api_keys": "КĐģŅŽŅ‡Ņ– API", "app_architecture_variant": "Đ’Đ°Ņ€Ņ–Đ°ĐŊŅ‚ (ĐŅ€Ņ…Ņ–Ņ‚ĐĩĐēŅ‚ŅƒŅ€Đ°)", @@ -541,179 +541,179 @@ "app_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃", "app_stores": "ĐœĐ°ĐŗĐ°ĐˇĐ¸ĐŊи ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐēŅ–Đ˛", "app_update_available": "ОĐŊОвĐģĐĩĐŊĐŊŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ Đ´ĐžŅŅ‚ŅƒĐŋĐŊĐĩ", - "appears_in": "З'ŅĐ˛ĐģŅŅ”Ņ‚ŅŒŅŅ в", + "appears_in": "Đ¤Ņ–ĐŗŅƒŅ€ŅƒŅ” в", "apply_count": "Đ—Đ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ({count, number})", "archive": "ĐŅ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸", - "archive_action_prompt": "{count, plural, one {# Ņ„Đ°ĐšĐģ дОдаĐŊĐž Đ´Đž Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ} few {# Ņ„Đ°ĐšĐģи дОдаĐŊĐž Đ´Đž Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ} other {# Ņ„Đ°ĐšĐģŅ–Đ˛ дОдаĐŊĐž Đ´Đž Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ}}", - "archive_or_unarchive_photo": "ĐŅ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸ айО Ņ€ĐžĐˇĐ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž", - "archive_page_no_archived_assets": "НĐĩĐŧĐ°Ņ” Đ°Ņ€Ņ…Ņ–Đ˛ĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", + "archive_action_prompt": "{count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ дОдаĐŊĐž Đ´Đž Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ дОдаĐŊĐž Đ´Đž Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ дОдаĐŊĐž Đ´Đž Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ дОдаĐŊĐž Đ´Đž Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ}}", + "archive_or_unarchive_photo": "ĐŅ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸ айО виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ Ņ„ĐžŅ‚Đž", + "archive_page_no_archived_assets": "НĐĩĐŧĐ°Ņ” Đ°Ņ€Ņ…Ņ–Đ˛ĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", "archive_page_title": "ĐŅ€Ņ…Ņ–Đ˛ ({count})", "archive_size": "РОСĐŧŅ–Ņ€ Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ", "archive_size_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ€ĐžĐˇĐŧŅ–Ņ€ Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ Đ´ĐģŅ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ (҃ GiB)", - "archived": "ĐŅ€Ņ…Ņ–Đ˛", - "archived_count": "{count, plural, other {ĐŅ€Ņ…Ņ–Đ˛ĐžĐ˛Đ°ĐŊĐž #}}", + "archived": "Đ—Đ°Đ°Ņ€Ņ…Ņ–Đ˛ĐžĐ˛Đ°ĐŊĐž", + "archived_count": "{count, plural, one {ĐŅ€Ņ…Ņ–Đ˛ĐžĐ˛Đ°ĐŊĐž #} few {ĐŅ€Ņ…Ņ–Đ˛ĐžĐ˛Đ°ĐŊĐž #} many {ĐŅ€Ņ…Ņ–Đ˛ĐžĐ˛Đ°ĐŊĐž #} other {ĐŅ€Ņ…Ņ–Đ˛ĐžĐ˛Đ°ĐŊĐž #}}", "are_these_the_same_person": "ĐĻĐĩ Ņ‚Đ° ŅĐ°Đŧа ĐģŅŽĐ´Đ¸ĐŊа?", "are_you_sure_to_do_this": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ҆Đĩ ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸?", "array_field_not_fully_supported": "ПоĐģŅ ĐŧĐ°ŅĐ¸Đ˛Ņƒ ĐŋĐžŅ‚Ņ€ĐĩĐąŅƒŅŽŅ‚ŅŒ Ņ€ŅƒŅ‡ĐŊĐžĐŗĐž Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°ĐŊĐŊŅ JSON", - "asset_action_delete_err_read_only": "НĐĩĐŧĐžĐļĐģивО видаĐģĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ(и) ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", - "asset_action_share_err_offline": "НĐĩĐŧĐžĐļĐģивО ĐžĐŋŅ€Đ°Ņ†ŅŽĐ˛Đ°Ņ‚Đ¸ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊŅ– Ņ„Đ°ĐšĐģ(и), ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", + "asset_action_delete_err_read_only": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "asset_action_share_err_offline": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", "asset_added_to_album": "ДодаĐŊĐž Đ´Đž аĐģŅŒĐąĐžĐŧ҃", - "asset_adding_to_album": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧ҃â€Ļ", - "asset_created": "ФаКĐģ дОдаĐŊĐž", - "asset_description_updated": "ОĐŊОвĐģĐĩĐŊĐž ĐžĐŋĐ¸Ņ Ņ„Đ°ĐšĐģ҃", - "asset_filename_is_offline": "ФаКĐģ {filename} ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиК", - "asset_has_unassigned_faces": "Є ĐŊĐĩŅ€ĐžĐˇĐŋŅ–ĐˇĐŊаĐŊŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ", + "asset_adding_to_album": "ДодаваĐŊĐŊŅ Đ´Đž аĐģŅŒĐąĐžĐŧ҃â€Ļ", + "asset_created": "ЕĐģĐĩĐŧĐĩĐŊŅ‚ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž", + "asset_description_updated": "ОĐŊОвĐģĐĩĐŊĐž ĐžĐŋĐ¸Ņ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°", + "asset_filename_is_offline": "ЕĐģĐĩĐŧĐĩĐŊŅ‚ {filename} ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиК", + "asset_has_unassigned_faces": "Є ĐŊĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ", "asset_hashing": "ĐĨĐĩŅˆŅƒĐ˛Đ°ĐŊĐŊŅâ€Ļ", "asset_list_group_by_sub_title": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ Са", - "asset_list_layout_settings_dynamic_layout_title": "ДиĐŊаĐŧҖ҇ĐŊĐĩ ĐēĐžĐŧĐŋĐžĐŊŅƒĐ˛Đ°ĐŊĐŊŅ", + "asset_list_layout_settings_dynamic_layout_title": "ДиĐŊаĐŧҖ҇ĐŊиК ĐŧаĐēĐĩŅ‚", "asset_list_layout_settings_group_automatically": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž", - "asset_list_layout_settings_group_by": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ĐŋĐž", + "asset_list_layout_settings_group_by": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ Са", "asset_list_layout_settings_group_by_month_day": "ĐœŅ–ŅŅŅ†ŅŒ + Đ´ĐĩĐŊҌ", - "asset_list_layout_sub_title": "РОСĐŧŅ–Ņ‚Đēа", + "asset_list_layout_sub_title": "МаĐēĐĩŅ‚", "asset_list_settings_subtitle": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛Đ¸ĐŗĐģŅĐ´Ņƒ ҁҖ҂Đēи Ņ„ĐžŅ‚Đž", - "asset_list_settings_title": "Đ¤ĐžŅ‚Đž-ҁҖ҂Đēа", - "asset_not_found_on_device_android": "ФаКĐģ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", - "asset_not_found_on_device_ios": "ФаКĐģ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—. Đ¯ĐēŅ‰Đž ви виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ iCloud, Ņ„Đ°ĐšĐģ ĐŧĐžĐļĐĩ ĐąŅƒŅ‚Đ¸ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиĐŧ ҇ĐĩŅ€ĐĩС ĐŋĐžŅˆĐēОдĐļĐĩĐŊиК Ņ„Đ°ĐšĐģ, Ņ‰Đž СйĐĩŅ€Ņ–ĐŗĐ°Ņ”Ņ‚ŅŒŅŅ в iCloud", - "asset_not_found_on_icloud": "ФаКĐģ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž в iCloud. МоĐļĐģивО, Ņ„Đ°ĐšĐģ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиК ҇ĐĩŅ€ĐĩС ĐŋĐžŅˆĐēОдĐļĐĩĐŊиК Ņ„Đ°ĐšĐģ, Ņ‰Đž СйĐĩŅ€Ņ–ĐŗĐ°Ņ”Ņ‚ŅŒŅŅ в iCloud", - "asset_offline": "ФаКĐģ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиК", - "asset_offline_description": "ĐĻĐĩĐš Ņ„Đ°ĐšĐģ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐŊа Đ´Đ¸ŅĐē҃. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, СвĐĩŅ€ĐŊŅ–Ņ‚ŅŒŅŅ Đ´Đž адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ° Immich Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ.", - "asset_restored_successfully": "ФаКĐģ ҃ҁĐŋŅ–ŅˆĐŊĐž Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž", + "asset_list_settings_title": "Đ¤ĐžŅ‚ĐžŅŅ–Ņ‚Đēа", + "asset_not_found_on_device_android": "ЕĐģĐĩĐŧĐĩĐŊŅ‚ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", + "asset_not_found_on_device_ios": "ЕĐģĐĩĐŧĐĩĐŊŅ‚ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—. Đ¯ĐēŅ‰Đž ви виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ iCloud, ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ ĐŧĐžĐļĐĩ ĐąŅƒŅ‚Đ¸ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиĐŧ ҇ĐĩŅ€ĐĩС ĐŋĐžŅˆĐēОдĐļĐĩĐŊиК Ņ„Đ°ĐšĐģ, Ņ‰Đž СйĐĩŅ€Ņ–ĐŗĐ°Ņ”Ņ‚ŅŒŅŅ в iCloud", + "asset_not_found_on_icloud": "ЕĐģĐĩĐŧĐĩĐŊŅ‚ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž в iCloud. МоĐļĐģивО, ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиК ҇ĐĩŅ€ĐĩС ĐŋĐžŅˆĐēОдĐļĐĩĐŊиК Ņ„Đ°ĐšĐģ, Ņ‰Đž СйĐĩŅ€Ņ–ĐŗĐ°Ņ”Ņ‚ŅŒŅŅ в iCloud", + "asset_offline": "ЕĐģĐĩĐŧĐĩĐŊŅ‚ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиК", + "asset_offline_description": "ĐĻĐĩĐš СОвĐŊŅ–ŅˆĐŊŅ–Đš ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐŊа Đ´Đ¸ŅĐē҃. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, СвĐĩŅ€ĐŊŅ–Ņ‚ŅŒŅŅ Đ´Đž адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ° Immich Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ.", + "asset_restored_successfully": "ЕĐģĐĩĐŧĐĩĐŊŅ‚ Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž", "asset_skipped": "ĐŸŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", "asset_skipped_in_trash": "ĐŖ ĐēĐžŅˆĐ¸Đē҃", - "asset_trashed": "ФаКĐģ видаĐģĐĩĐŊĐž", - "asset_troubleshoot": "Đ’Đ¸Ņ€Ņ–ŅˆĐĩĐŊĐŊŅ ĐŋŅ€ĐžĐąĐģĐĩĐŧ С Ņ„Đ°ĐšĐģаĐŧи", + "asset_trashed": "ЕĐģĐĩĐŧĐĩĐŊŅ‚ ĐŋĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа", + "asset_troubleshoot": "Đ’Đ¸Ņ€Ņ–ŅˆĐĩĐŊĐŊŅ ĐŋŅ€ĐžĐąĐģĐĩĐŧ Ņ–Đˇ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°Đŧи", "asset_uploaded": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐž", "asset_uploading": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅâ€Ļ", "asset_viewer_settings_subtitle": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‡Đ° ĐŗĐ°ĐģĐĩŅ€ĐĩŅ—", - "asset_viewer_settings_title": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‡ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ", - "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 {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} Đ´Đž {albumTotal, plural, one {# аĐģŅŒĐąĐžĐŧ҃} few {# аĐģŅŒĐąĐžĐŧŅ–Đ˛} many {# аĐģŅŒĐąĐžĐŧŅ–Đ˛} other {# аĐģŅŒĐąĐžĐŧŅ–Đ˛}}", - "assets_cannot_be_added_to_album_count": "{count, plural, one {ФаКĐģ} few {ФаКĐģи} many {ФаКĐģи} other {ФаКĐģи}} ĐŊĐĩ ĐŧĐžĐļĐŊа Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", - "assets_cannot_be_added_to_albums": "{count, plural, one {ФаКĐģ} few {ФаКĐģи} many {ФаКĐģŅ–Đ˛} other {ФаКĐģŅ–Đ˛}} ĐŊĐĩ ĐŧĐžĐļĐŊа Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž ĐļОдĐŊĐžĐŗĐž С аĐģŅŒĐąĐžĐŧŅ–Đ˛", - "assets_count": "{count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", - "assets_deleted_permanently": "ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", - "assets_deleted_permanently_from_server": "ВидаĐģĐĩĐŊĐž ĐŊаСавĐļди {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} С ҁĐĩŅ€Đ˛ĐĩŅ€Đ° Immich", - "assets_downloaded_failed": "{count, plural, one {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģ — {error} ĐŊĐĩ вдаĐģĐžŅŅ} few {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģи — {error} ĐŊĐĩ вдаĐģĐžŅŅ} many {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģŅ–Đ˛ — {error} ĐŊĐĩ вдаĐģĐžŅŅ} other {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģŅ–Đ˛ — {error} ĐŊĐĩ вдаĐģĐžŅŅ}}", - "assets_downloaded_successfully": "{count, plural, one {ĐŖŅĐŋŅ–ŅˆĐŊĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģ} few {ĐŖŅĐŋŅ–ŅˆĐŊĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģи} many {ĐŖŅĐŋŅ–ŅˆĐŊĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģŅ–Đ˛} 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, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", - "assets_restore_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Đ˛ŅŅ– ŅĐ˛ĐžŅ— Ņ„Đ°ĐšĐģи С ĐēĐžŅˆĐ¸Đēа? ĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸! ЗвĐĩŅ€ĐŊŅ–Ņ‚ŅŒ ŅƒĐ˛Đ°ĐŗŅƒ, Ņ‰Đž ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊŅ– Ņ„Đ°ĐšĐģи ĐŊĐĩ ĐŧĐžĐļŅƒŅ‚ŅŒ ĐąŅƒŅ‚Đ¸ Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊŅ– Ņ‚Đ°ĐēиĐŧ Ņ‡Đ¸ĐŊĐžĐŧ.", - "assets_restored_count": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", - "assets_restored_successfully": "ĐŖŅĐŋŅ–ŅˆĐŊĐž Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", - "assets_trashed": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", - "assets_trashed_count": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", - "assets_trashed_from_server": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ Immich {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", - "assets_were_part_of_album_count": "{count, plural, one {ФаКĐģ ĐąŅƒĐ˛} few {ФаКĐģи ĐąŅƒĐģи} other {ФаКĐģи ĐąŅƒĐģи}} вĐļĐĩ Ņ‡Đ°ŅŅ‚Đ¸ĐŊĐžŅŽ аĐģŅŒĐąĐžĐŧ҃", - "assets_were_part_of_albums_count": "{count, plural, one {ФаКĐģ вĐļĐĩ ĐąŅƒĐ˛} few {ФаКĐģи вĐļĐĩ ĐąŅƒĐģи} many {ФаКĐģŅ–Đ˛ вĐļĐĩ ĐąŅƒĐģи} other {ФаКĐģŅ–Đ˛ вĐļĐĩ ĐąŅƒĐģи}} Ņ‡Đ°ŅŅ‚Đ¸ĐŊĐžŅŽ аĐģŅŒĐąĐžĐŧŅ–Đ˛", + "asset_viewer_settings_title": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‡ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", + "assets": "ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", + "assets_added_count": "ДодаĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "assets_added_to_album_count": "ДодаĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} Đ´Đž аĐģŅŒĐąĐžĐŧ҃", + "assets_added_to_albums_count": "ДодаĐŊĐž {assetTotal, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} Đ´Đž {albumTotal, plural, one {# аĐģŅŒĐąĐžĐŧ҃} few {# аĐģŅŒĐąĐžĐŧŅ–Đ˛} many {# аĐģŅŒĐąĐžĐŧŅ–Đ˛} other {# аĐģŅŒĐąĐžĐŧŅ–Đ˛}}", + "assets_cannot_be_added_to_album_count": "{count, plural, one {ЕĐģĐĩĐŧĐĩĐŊŅ‚} few {ЕĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {ЕĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {ЕĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} ĐŊĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", + "assets_cannot_be_added_to_albums": "{count, plural, one {ЕĐģĐĩĐŧĐĩĐŊŅ‚} few {ЕĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {ЕĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {ЕĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} ĐŊĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž ĐļОдĐŊĐžĐŗĐž С аĐģŅŒĐąĐžĐŧŅ–Đ˛", + "assets_count": "{count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "assets_deleted_permanently": "НазавĐļди видаĐģĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "assets_deleted_permanently_from_server": "ВидаĐģĐĩĐŊĐž ĐŊаСавĐļди {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} Ņ–Đˇ ҁĐĩŅ€Đ˛ĐĩŅ€Đ° Immich", + "assets_downloaded_failed": "{count, plural, one {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģ — ĐŊĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ {error}} few {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģи — ĐŊĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ {error}} many {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģŅ–Đ˛ — ĐŊĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ {error}} other {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģŅ–Đ˛ — ĐŊĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ {error}}}", + "assets_downloaded_successfully": "{count, plural, one {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģ} few {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģи} many {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģŅ–Đ˛} other {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "assets_moved_to_trash_count": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} Đ´Đž ĐēĐžŅˆĐ¸Đēа", + "assets_permanently_deleted_count": "ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "assets_removed_count": "ВиĐģŅƒŅ‡ĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "assets_removed_permanently_from_device": "НазавĐļди виĐģŅƒŅ‡ĐĩĐŊĐž С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "assets_restore_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Đ˛ŅŅ– ŅĐ˛ĐžŅ— ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ С ĐēĐžŅˆĐ¸Đēа? ĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸! ЗвĐĩŅ€ĐŊŅ–Ņ‚ŅŒ ŅƒĐ˛Đ°ĐŗŅƒ, Ņ‰Đž ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŊĐĩ Đ˛Đ´Đ°ŅŅ‚ŅŒŅŅ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ‚Đ°ĐēиĐŧ Ņ‡Đ¸ĐŊĐžĐŧ.", + "assets_restored_count": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "assets_restored_successfully": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "assets_trashed": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "assets_trashed_count": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "assets_trashed_from_server": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ Immich {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "assets_were_part_of_album_count": "{count, plural, one {ЕĐģĐĩĐŧĐĩĐŊŅ‚ ĐąŅƒĐ˛} few {ЕĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐąŅƒĐģи} many {ЕĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ĐąŅƒĐģĐž} other {ЕĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ĐąŅƒĐģĐž}} вĐļĐĩ Ņ‡Đ°ŅŅ‚Đ¸ĐŊĐžŅŽ аĐģŅŒĐąĐžĐŧ҃", + "assets_were_part_of_albums_count": "{count, plural, one {ЕĐģĐĩĐŧĐĩĐŊŅ‚ вĐļĐĩ ĐąŅƒĐ˛} few {ЕĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ вĐļĐĩ ĐąŅƒĐģи} many {ЕĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ вĐļĐĩ ĐąŅƒĐģĐž} other {ЕĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ вĐļĐĩ ĐąŅƒĐģĐž}} Ņ‡Đ°ŅŅ‚Đ¸ĐŊĐžŅŽ аĐģŅŒĐąĐžĐŧŅ–Đ˛", "authorized_devices": "ĐĐ˛Ņ‚ĐžŅ€Đ¸ĐˇĐžĐ˛Đ°ĐŊŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", - "automatic_endpoint_switching_subtitle": "ĐŸŅ–Đ´ĐēĐģŅŽŅ‡Đ°Ņ‚Đ¸ŅŅ ĐģĐžĐēаĐģҌĐŊĐž ҇ĐĩŅ€ĐĩС СаСĐŊĐ°Ņ‡ĐĩĐŊ҃ Wi-Fi ĐŧĐĩŅ€ĐĩĐļ҃, ĐēĐžĐģи ҆Đĩ ĐŧĐžĐļĐģивО, Ņ– виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛ĐŊŅ– С'Ņ”Đ´ĐŊаĐŊĐŊŅ в Ņ–ĐŊŅˆĐ¸Ņ… виĐŋадĐēĐ°Ņ…", + "automatic_endpoint_switching_subtitle": "ĐŸŅ–Đ´'Ņ”Đ´ĐŊŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ ĐģĐžĐēаĐģҌĐŊĐž ҇ĐĩŅ€ĐĩС СаСĐŊĐ°Ņ‡ĐĩĐŊ҃ ĐŧĐĩŅ€ĐĩĐļ҃ Wi-Fi, ĐēĐžĐģи ҆Đĩ ĐŧĐžĐļĐģивО, Ņ– виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛ĐŊŅ– С'Ņ”Đ´ĐŊаĐŊĐŊŅ в Ņ–ĐŊŅˆĐ¸Ņ… виĐŋадĐēĐ°Ņ…", "automatic_endpoint_switching_title": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐĩ ĐŋĐĩŅ€ĐĩĐŧиĐēаĐŊĐŊŅ URL", - "autoplay_slideshow": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐĩ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ҁĐģĐ°ĐšĐ´ŅˆĐžŅƒ", + "autoplay_slideshow": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐĩ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ҁĐģаКд-ŅˆĐžŅƒ", "back": "Назад", "back_close_deselect": "ПовĐĩŅ€ĐŊŅƒŅ‚Đ¸ŅŅ, СаĐēŅ€Đ¸Ņ‚Đ¸ айО ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛Đ¸ĐąŅ–Ņ€", - "background_backup_running_error": "ĐĐ°Ņ€Đ°ĐˇŅ– виĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ Ņ„ĐžĐŊОвĐĩ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ, ĐŊĐĩĐŧĐžĐļĐģивО Ņ€ĐžĐˇĐŋĐžŅ‡Đ°Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Đ˛Ņ€ŅƒŅ‡ĐŊ҃", - "background_location_permission": "Đ”ĐžĐˇĐ˛Ņ–Đģ Đ´Đž ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ ҃ Ņ„ĐžĐŊŅ–", - "background_location_permission_content": "ЊОй ĐŋĐĩŅ€ĐĩĐŧиĐēĐ°Ņ‚Đ¸ ĐŧĐĩŅ€ĐĩĐļŅ– ҃ Ņ„ĐžĐŊОвОĐŧ҃ Ņ€ĐĩĐļиĐŧŅ–, Immich ĐŧĐ°Ņ” *СавĐļди* ĐŧĐ°Ņ‚Đ¸ Đ´ĐžŅŅ‚ŅƒĐŋ Đ´Đž Ņ‚ĐžŅ‡ĐŊĐžŅ— ĐŗĐĩĐžĐģĐžĐēĐ°Ņ†Ņ–Ņ—, Ņ‰ĐžĐą ĐˇŅ‡Đ¸Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐ°ĐˇĐ˛Ņƒ Wi-Fi ĐŧĐĩŅ€ĐĩĐļŅ–", - "background_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ Ņ„ĐžĐŊ҃", + "background_backup_running_error": "ĐĐ°Ņ€Đ°ĐˇŅ– виĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ Ņ„ĐžĐŊОвĐĩ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ, ĐŊĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Ņ€ĐžĐˇĐŋĐžŅ‡Đ°Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Đ˛Ņ€ŅƒŅ‡ĐŊ҃", + "background_location_permission": "Đ”ĐžĐˇĐ˛Ņ–Đģ ĐŊа виСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŧŅ–ŅŅ†Ņ ҃ Ņ„ĐžĐŊОвОĐŧ҃ Ņ€ĐĩĐļиĐŧŅ–", + "background_location_permission_content": "ДĐģŅ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŧиĐēаĐŊĐŊŅ ĐŧŅ–Đļ ĐŧĐĩŅ€ĐĩĐļаĐŧи ҃ Ņ„ĐžĐŊОвОĐŧ҃ Ņ€ĐĩĐļиĐŧŅ– Immich ĐŋĐžŅ‚Ņ€ĐĩĐąŅƒŅ” *ĐŋĐžŅŅ‚Ņ–ĐšĐŊĐžĐŗĐž* Đ´ĐžŅŅ‚ŅƒĐŋ҃ Đ´Đž Ņ‚ĐžŅ‡ĐŊĐžĐŗĐž Ņ€ĐžĐˇŅ‚Đ°ŅˆŅƒĐ˛Đ°ĐŊĐŊŅ, Ņ‰ĐžĐą ĐˇŅ‡Đ¸Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐ°ĐˇĐ˛Ņƒ ĐŧĐĩŅ€ĐĩĐļŅ– Wi-Fi", + "background_options": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ„ĐžĐŊĐžĐ˛ĐžĐŗĐž Ņ€ĐĩĐļиĐŧ҃", "backup": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", "backup_album_selection_page_albums_device": "АĐģŅŒĐąĐžĐŧи ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— ({count})", - "backup_album_selection_page_albums_tap": "ĐĸĐžŅ€ĐēĐŊŅ–Ņ‚ŅŒŅŅ, Ņ‰ĐžĐą Đ´ĐžĐ´Đ°Ņ‚Đ¸, Đ´Đ˛Ņ–Ņ‡Ņ–, Ņ‰ĐžĐą виĐģŅƒŅ‡Đ¸Ņ‚Đ¸", - "backup_album_selection_page_assets_scatter": "ФаКĐģи ĐŧĐžĐļŅƒŅ‚ŅŒ ĐŊаĐģĐĩĐļĐ°Ņ‚Đ¸ Đ´Đž ĐēŅ–ĐģҌĐēĐžŅ… аĐģŅŒĐąĐžĐŧŅ–Đ˛ вОдĐŊĐžŅ‡Đ°Ņ. ĐĸаĐēиĐŧ Ņ‡Đ¸ĐŊĐžĐŧ, аĐģŅŒĐąĐžĐŧи ĐŧĐžĐļŅƒŅ‚ŅŒ ĐąŅƒŅ‚Đ¸ дОдаĐŊŅ– айО виĐģŅƒŅ‡ĐĩĐŊŅ– ĐŋŅ–Đ´ Ņ‡Đ°Ņ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ.", - "backup_album_selection_page_select_albums": "ОбĐĩŅ€Ņ–Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧи", - "backup_album_selection_page_selection_info": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž ĐžĐąŅ€Đ°ĐŊĐĩ", - "backup_album_selection_page_total_assets": "Đ—Đ°ĐŗĐ°ĐģҌĐŊа ĐēŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ҃ĐŊŅ–ĐēаĐģҌĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", + "backup_album_selection_page_albums_tap": "ОдĐŊĐĩ Ņ‚ĐžŅ€ĐēаĐŊĐŊŅ — Đ´ĐžĐ´Đ°Ņ‚Đ¸, ĐŋĐžĐ´Đ˛Ņ–ĐšĐŊĐĩ — виĐģŅƒŅ‡Đ¸Ņ‚Đ¸", + "backup_album_selection_page_assets_scatter": "ЕĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŧĐžĐļŅƒŅ‚ŅŒ ĐŊаĐģĐĩĐļĐ°Ņ‚Đ¸ Đ´Đž ĐēŅ–ĐģҌĐēĐžŅ… аĐģŅŒĐąĐžĐŧŅ–Đ˛ вОдĐŊĐžŅ‡Đ°Ņ. ĐĸаĐēиĐŧ Ņ‡Đ¸ĐŊĐžĐŧ, аĐģŅŒĐąĐžĐŧи ĐŧĐžĐļĐŊа Đ˛Ņ€Đ°Ņ…ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ айО ĐŊĐĩ Đ˛Ņ€Đ°Ņ…ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋŅ–Đ´ Ņ‡Đ°Ņ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ.", + "backup_album_selection_page_select_albums": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи", + "backup_album_selection_page_selection_info": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž Đ˛Đ¸ĐąŅ€Đ°ĐŊĐĩ", + "backup_album_selection_page_total_assets": "Đ—Đ°ĐŗĐ°ĐģҌĐŊа ĐēŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ҃ĐŊŅ–ĐēаĐģҌĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", "backup_albums_sync": "ХиĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐ¸Ņ… ĐēĐžĐŋŅ–Đš аĐģŅŒĐąĐžĐŧŅ–Đ˛", "backup_all": "ĐŖŅŅ–", - "backup_background_service_backup_failed_message": "НĐĩ вдаĐģĐžŅŅ ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ Ņ„Đ°ĐšĐģŅ–Đ˛. ĐŸĐžĐ˛Ņ‚ĐžŅ€ŅŽŅŽâ€Ļ", - "backup_background_service_complete_notification": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Ņ„Đ°ĐšĐģŅ–Đ˛ СавĐĩŅ€ŅˆĐĩĐŊĐž", + "backup_background_service_backup_failed_message": "НĐĩ вдаĐģĐžŅŅ ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛. ĐŸĐžĐ˛Ņ‚ĐžŅ€ŅŽŅŽâ€Ļ", + "backup_background_service_complete_notification": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ СавĐĩŅ€ŅˆĐĩĐŊĐž", "backup_background_service_connection_failed_message": "НĐĩ вдаĐģĐžŅŅ Св'ŅĐˇĐ°Ņ‚Đ¸ŅŅ Ņ–Đˇ ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ. ĐŸĐžĐ˛Ņ‚ĐžŅ€ŅŽŅŽâ€Ļ", "backup_background_service_current_upload_notification": "ВиваĐŊŅ‚Đ°ĐļŅƒŅ”Ņ‚ŅŒŅŅ {filename}", - "backup_background_service_default_notification": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€ŅŅŽ ĐŊĐ°ŅĐ˛ĐŊŅ–ŅŅ‚ŅŒ ĐŊĐžĐ˛Đ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛â€Ļ", - "backup_background_service_error_title": "ПоĐŧиĐģĐēа Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", - "backup_background_service_in_progress_notification": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Đ˛Đ°ŅˆĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛â€Ļ", + "backup_background_service_default_notification": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€ŅŅŽ ĐŊĐ°ŅĐ˛ĐŊŅ–ŅŅ‚ŅŒ ĐŊĐžĐ˛Đ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛â€Ļ", + "backup_background_service_error_title": "Đ—ĐąŅ–Đš Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", + "backup_background_service_in_progress_notification": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Đ˛Đ°ŅˆĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛â€Ļ", "backup_background_service_upload_failure_notification": "НĐĩ вдаĐģĐžŅŅ виваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ {filename}", "backup_controller_page_albums": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ аĐģŅŒĐąĐžĐŧŅ–Đ˛", - "backup_controller_page_background_app_refresh_disabled_content": "ДĐģŅ Ņ„ĐžĐŊĐžĐ˛ĐžĐŗĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ŅƒĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ Ņ„ĐžĐŊОвĐĩ ĐžĐŊОвĐģĐĩĐŊĐŊŅ в ĐŧĐĩĐŊŅŽ \"НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ > Đ—Đ°ĐŗĐ°ĐģҌĐŊŅ– > ФОĐŊОвĐĩ ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃\".", - "backup_controller_page_background_app_refresh_disabled_title": "ФОĐŊОвĐĩ ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ виĐŧĐēĐŊĐĩĐŊĐĩ", + "backup_controller_page_background_app_refresh_disabled_content": "ДĐģŅ Ņ„ĐžĐŊĐžĐ˛ĐžĐŗĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ŅƒĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ Ņ„ĐžĐŊОвĐĩ ĐžĐŊОвĐģĐĩĐŊĐŊŅ в ĐŧĐĩĐŊŅŽ ÂĢНаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ > Đ—Đ°ĐŗĐ°ĐģҌĐŊŅ– > ФОĐŊОвĐĩ ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃Âģ.", + "backup_controller_page_background_app_refresh_disabled_title": "ФОĐŊОвĐĩ ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ виĐŧĐēĐŊĐĩĐŊĐž", "backup_controller_page_background_app_refresh_enable_button_text": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ", "backup_controller_page_background_battery_info_link": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ŅĐē", - "backup_controller_page_background_battery_info_message": "ДĐģŅ ĐŊаКĐēŅ€Đ°Ņ‰ĐžĐŗĐž Ņ„ĐžĐŊĐžĐ˛ĐžĐŗĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ виĐŧĐēĐŊŅ–Ņ‚ŅŒ ĐąŅƒĐ´ŅŒ-ŅĐē҃ ĐžĐŋŅ‚Đ¸ĐŧŅ–ĐˇĐ°Ņ†Ņ–ŅŽ аĐē҃Đŧ҃ĐģŅŅ‚ĐžŅ€Đ°, ŅĐēа ОйĐŧĐĩĐļŅƒŅ” Ņ„ĐžĐŊĐžĐ˛Ņƒ аĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ Đ´ĐģŅ Immich.\n\nĐĄĐŋĐžŅŅ–Đą СаĐģĐĩĐļĐ¸Ņ‚ŅŒ Đ˛Ņ–Đ´ ĐēĐžĐŊĐēŅ€ĐĩŅ‚ĐŊĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ, Ņ‚ĐžĐŧ҃ ҈҃ĐēĐ°ĐšŅ‚Đĩ ĐŊĐĩĐžĐąŅ…Ņ–Đ´ĐŊ҃ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–ŅŽ ҃ Đ˛Đ¸Ņ€ĐžĐąĐŊиĐēа Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ.", + "backup_controller_page_background_battery_info_message": "ДĐģŅ ĐŊаКĐēŅ€Đ°Ņ‰ĐžĐŗĐž Ņ„ĐžĐŊĐžĐ˛ĐžĐŗĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ виĐŧĐēĐŊŅ–Ņ‚ŅŒ ĐąŅƒĐ´ŅŒ-ŅĐē҃ ĐžĐŋŅ‚Đ¸ĐŧŅ–ĐˇĐ°Ņ†Ņ–ŅŽ аĐē҃Đŧ҃ĐģŅŅ‚ĐžŅ€Đ°, ŅĐēа ОйĐŧĐĩĐļŅƒŅ” Ņ„ĐžĐŊĐžĐ˛Ņƒ аĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ Đ´ĐģŅ Immich.\n\nĐžŅĐēŅ–ĐģҌĐēи ҆Đĩ СаĐģĐĩĐļĐ¸Ņ‚ŅŒ Đ˛Ņ–Đ´ ĐēĐžĐŊĐēŅ€ĐĩŅ‚ĐŊĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ, СĐŊĐ°ĐšĐ´Ņ–Ņ‚ŅŒ ĐŊĐĩĐžĐąŅ…Ņ–Đ´ĐŊ҃ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–ŅŽ ҃ Đ˛Đ¸Ņ€ĐžĐąĐŊиĐēа Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ.", "backup_controller_page_background_battery_info_ok": "ОК", - "backup_controller_page_background_battery_info_title": "ОĐŋŅ‚Đ¸ĐŧŅ–ĐˇĐ°Ņ†Ņ–Ņ ĐąĐ°Ņ‚Đ°Ņ€ĐĩŅ—", + "backup_controller_page_background_battery_info_title": "ОĐŋŅ‚Đ¸ĐŧŅ–ĐˇĐ°Ņ†Ņ–Ņ аĐē҃Đŧ҃ĐģŅŅ‚ĐžŅ€Đ°", "backup_controller_page_background_charging": "Đ›Đ¸ŅˆĐĩ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐˇĐ°Ņ€ŅĐ´ĐļаĐŊĐŊŅ", - "backup_controller_page_background_configure_error": "НĐĩ вдаĐģĐžŅŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„ĐžĐŊОвиК ҁĐĩŅ€Đ˛Ņ–Ņ", - "backup_controller_page_background_delay": "Đ—Đ°Ņ‚Ņ€Đ¸ĐŧĐēа Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ĐŊĐžĐ˛Đ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛: {duration}", - "backup_controller_page_background_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ Ņ„ĐžĐŊĐžĐ˛Ņƒ ҁĐģ҃ĐļĐąŅƒ, Ņ‰ĐžĐą Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž ŅŅ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— ĐąŅƒĐ´ŅŒ-ŅĐēĐ¸Ņ… ĐŊĐžĐ˛Đ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛ ĐąĐĩС ĐŊĐĩĐžĐąŅ…Ņ–Đ´ĐŊĐžŅŅ‚Ņ– Đ˛Ņ–Đ´ĐēŅ€Đ¸Đ˛Đ°Ņ‚Đ¸ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē", + "backup_controller_page_background_configure_error": "НĐĩ вдаĐģĐžŅŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„ĐžĐŊĐžĐ˛Ņƒ ҁĐģ҃ĐļĐąŅƒ", + "backup_controller_page_background_delay": "Đ—Đ°Ņ‚Ņ€Đ¸ĐŧĐēа Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ĐŊĐžĐ˛Đ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛: {duration}", + "backup_controller_page_background_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ Ņ„ĐžĐŊĐžĐ˛Ņƒ ҁĐģ҃ĐļĐąŅƒ, Ņ‰ĐžĐą Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž ŅŅ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— ĐąŅƒĐ´ŅŒ-ŅĐēĐ¸Ņ… ĐŊĐžĐ˛Đ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ĐąĐĩС ĐŋĐžŅ‚Ņ€Đĩйи Đ˛Ņ–Đ´ĐēŅ€Đ¸Đ˛Đ°Ņ‚Đ¸ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē", "backup_controller_page_background_is_off": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐĩ Ņ„ĐžĐŊОвĐĩ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ виĐŧĐēĐŊĐĩĐŊĐž", - "backup_controller_page_background_is_on": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐĩ Ņ„ĐžĐŊОвĐĩ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Đ˛Đ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž", - "backup_controller_page_background_turn_off": "ВиĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ„ĐžĐŊОвиК ҁĐĩŅ€Đ˛Ņ–Ņ", - "backup_controller_page_background_turn_on": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ„ĐžĐŊОвиК ҁĐĩŅ€Đ˛Ņ–Ņ", + "backup_controller_page_background_is_on": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐĩ Ņ„ĐžĐŊОвĐĩ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž", + "backup_controller_page_background_turn_off": "ВиĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ„ĐžĐŊĐžĐ˛Ņƒ ҁĐģ҃ĐļĐąŅƒ", + "backup_controller_page_background_turn_on": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ„ĐžĐŊĐžĐ˛Ņƒ ҁĐģ҃ĐļĐąŅƒ", "backup_controller_page_background_wifi": "Đ›Đ¸ŅˆĐĩ ĐŊа Wi-Fi", "backup_controller_page_backup": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", - "backup_controller_page_backup_selected": "ĐžĐąŅ€Đ°ĐŊĐž: ", + "backup_controller_page_backup_selected": "Đ’Đ¸ĐąŅ€Đ°ĐŊĐž: ", "backup_controller_page_backup_sub": "Đ ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", "backup_controller_page_created": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž: {date}", - "backup_controller_page_desc_backup": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ĐŊа ĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŧ҃ ĐŋĐģаĐŊŅ–, Ņ‰ĐžĐą Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž виваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€ ĐŋŅ–Đ´ Ņ‡Đ°Ņ Đ˛Ņ–Đ´ĐēŅ€Đ¸Ņ‚Ņ‚Ņ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃.", + "backup_controller_page_desc_backup": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ в аĐēŅ‚Đ¸Đ˛ĐŊĐžĐŧ҃ Ņ€ĐĩĐļиĐŧŅ–, Ņ‰ĐžĐą Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž виваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€ ĐŋŅ–Đ´ Ņ‡Đ°Ņ Đ˛Ņ–Đ´ĐēŅ€Đ¸Ņ‚Ņ‚Ņ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃.", "backup_controller_page_excluded": "ВиĐģŅƒŅ‡ĐĩĐŊĐž: ", "backup_controller_page_failed": "НĐĩвдаĐģŅ– ({count})", "backup_controller_page_filename": "Назва Ņ„Đ°ĐšĐģ҃: {filename} [{size}]", "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ", - "backup_controller_page_none_selected": "ĐŅ–Ņ‡ĐžĐŗĐž ĐŊĐĩ ĐžĐąŅ€Đ°ĐŊĐž", + "backup_controller_page_none_selected": "ĐŅ–Ņ‡ĐžĐŗĐž ĐŊĐĩ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž", "backup_controller_page_remainder": "ЗаĐģĐ¸ŅˆĐžĐē", - "backup_controller_page_remainder_sub": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž, Ņ‰Đž СаĐģĐ¸ŅˆĐ¸ĐģĐ¸ŅŅ Đ´ĐģŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ С Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž", + "backup_controller_page_remainder_sub": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž, ŅĐēŅ– СаĐģĐ¸ŅˆĐ¸ĐģĐžŅŅ ҁĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ С Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛", "backup_controller_page_server_storage": "ĐĄŅ…ĐžĐ˛Đ¸Ņ‰Đĩ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "backup_controller_page_start_backup": "ĐŸĐžŅ‡Đ°Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", "backup_controller_page_status_off": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐĩ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ в аĐēŅ‚Đ¸Đ˛ĐŊĐžĐŧ҃ Ņ€ĐĩĐļиĐŧŅ– виĐŧĐēĐŊĐĩĐŊĐž", - "backup_controller_page_status_on": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐĩ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ в аĐēŅ‚Đ¸Đ˛ĐŊĐžĐŧ҃ Ņ€ĐĩĐļиĐŧŅ– Đ˛Đ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž", + "backup_controller_page_status_on": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐĩ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ в аĐēŅ‚Đ¸Đ˛ĐŊĐžĐŧ҃ Ņ€ĐĩĐļиĐŧŅ– ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž", "backup_controller_page_storage_format": "ВиĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐž: {used} С {total}", "backup_controller_page_to_backup": "АĐģŅŒĐąĐžĐŧи Đ´Đž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", "backup_controller_page_total_sub": "ĐŖŅŅ– ҃ĐŊŅ–ĐēаĐģҌĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž С Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛", "backup_controller_page_turn_off": "ВиĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ в аĐēŅ‚Đ¸Đ˛ĐŊĐžĐŧ҃ Ņ€ĐĩĐļиĐŧŅ–", "backup_controller_page_turn_on": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ в аĐēŅ‚Đ¸Đ˛ĐŊĐžĐŧ҃ Ņ€ĐĩĐļиĐŧŅ–", - "backup_controller_page_uploading_file_info": "ВиваĐŊŅ‚Đ°ĐļŅƒŅŽ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–ŅŽ ĐŋŅ€Đž Ņ„Đ°ĐšĐģ", - "backup_err_only_album": "НĐĩ ĐŧĐžĐļ҃ видаĐģĐ¸Ņ‚Đ¸ Ņ”Đ´Đ¸ĐŊиК аĐģŅŒĐąĐžĐŧ", - "backup_error_sync_failed": "ПоĐŧиĐģĐēа ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ—. НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐąŅ€ĐžĐąĐ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ.", - "backup_info_card_assets": "Ņ„Đ°ĐšĐģи", + "backup_controller_page_uploading_file_info": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž Ņ„Đ°ĐšĐģ, Ņ‰Đž виваĐŊŅ‚Đ°ĐļŅƒŅ”Ņ‚ŅŒŅŅ", + "backup_err_only_album": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ Ņ”Đ´Đ¸ĐŊиК аĐģŅŒĐąĐžĐŧ", + "backup_error_sync_failed": "НĐĩ вдаĐģĐžŅŅ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇŅƒĐ˛Đ°Ņ‚Đ¸. НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ виĐēĐžĐŊĐ°Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ.", + "backup_info_card_assets": "ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", "backup_manual_cancelled": "ĐĄĐēĐ°ŅĐžĐ˛Đ°ĐŊĐž", "backup_manual_in_progress": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ вĐļĐĩ Đ˛Ņ–Đ´ĐąŅƒĐ˛Đ°Ņ”Ņ‚ŅŒŅŅ. ĐĄĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ ĐˇĐŗĐžĐ´ĐžĐŧ", - "backup_manual_success": "ĐŖŅĐŋŅ–Ņ…", + "backup_manual_success": "Đ“ĐžŅ‚ĐžĐ˛Đž", "backup_manual_title": "ĐĄŅ‚Đ°ĐŊ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", "backup_options": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", - "backup_options_page_title": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", + "backup_options_page_title": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", "backup_setting_subtitle": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ҃ Ņ„ĐžĐŊОвОĐŧ҃ Ņ‚Đ° аĐēŅ‚Đ¸Đ˛ĐŊĐžĐŧ҃ Ņ€ĐĩĐļиĐŧŅ–", "backup_settings_subtitle": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", "backup_upload_details_page_more_details": "ĐĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ, Ņ‰ĐžĐą Đ´Ņ–ĐˇĐŊĐ°Ņ‚Đ¸ŅŅ ĐąŅ–ĐģҌ҈Đĩ", "backward": "Назад", - "biometric_auth_enabled": "Đ‘Ņ–ĐžĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐŊа Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊа", - "biometric_locked_out": "ВаĐŧ СаĐēŅ€Đ¸Ņ‚Đž Đ´ĐžŅŅ‚ŅƒĐŋ Đ´Đž ĐąŅ–ĐžĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐŊĐžŅ— Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ—", - "biometric_no_options": "Đ‘Ņ–ĐžĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐŊŅ– ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊŅ–", + "biometric_auth_enabled": "Đ‘Ņ–ĐžĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐŊ҃ Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–ŅŽ ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž", + "biometric_locked_out": "Đ”ĐžŅŅ‚ŅƒĐŋ Đ´Đž ĐąŅ–ĐžĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐŊĐžŅ— Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ— СайĐģĐžĐēОваĐŊĐž", + "biometric_no_options": "Đ‘Ņ–ĐžĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐŊŅ– Đ˛Đ°Ņ€Ņ–Đ°ĐŊŅ‚Đ¸ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊŅ–", "biometric_not_available": "Đ‘Ņ–ĐžĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐŊа Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа ĐŊа Ņ†ŅŒĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", - "birthdate_saved": "Đ”Đ°Ņ‚Đ° ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ ҃ҁĐŋŅ–ŅˆĐŊĐž СйĐĩŅ€ĐĩĐļĐĩĐŊа", - "birthdate_set_description": "Đ”Đ°Ņ‚Đ° ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ Đ´ĐģŅ ĐžĐąŅ‡Đ¸ŅĐģĐĩĐŊĐŊŅ Đ˛Ņ–Đē҃ ҆ҖҔҗ ĐžŅĐžĐąĐ¸ ĐŊа ĐŧĐžĐŧĐĩĐŊŅ‚ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—.", + "birthdate_saved": "Đ”Đ°Ņ‚Ņƒ ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ СйĐĩŅ€ĐĩĐļĐĩĐŊĐž", + "birthdate_set_description": "Đ”Đ°Ņ‚Đ° ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ Đ´ĐģŅ ĐžĐąŅ‡Đ¸ŅĐģĐĩĐŊĐŊŅ Đ˛Ņ–Đē҃ ҆ҖҔҗ ĐģŅŽĐ´Đ¸ĐŊи ĐŊа ĐŧĐžĐŧĐĩĐŊŅ‚ Ņ„ĐžŅ‚Đž.", "blurred_background": "РОСĐŧĐ¸Ņ‚Đ¸Đš Ņ„ĐžĐŊ", - "bugs_and_feature_requests": "ПоĐŧиĐģĐēи Ņ‚Đ° ЗаĐŋĐ¸Ņ‚Đ¸", + "bugs_and_feature_requests": "Đ—Đ˛Ņ–Ņ‚Đ¸ ĐŋŅ€Đž ĐŋĐžĐŧиĐģĐēи Ņ‚Đ° ĐŋОйаĐļаĐŊĐŊŅ", "build": "Đ—ĐąŅ–Ņ€Đēа", - "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 {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛}}? ĐĻĐĩ СаĐģĐ¸ŅˆĐ¸Ņ‚ŅŒ ĐŊĐ°ĐšĐąŅ–ĐģŅŒŅˆĐ¸Đš Ņ„Đ°ĐšĐģ ҃ ĐēĐžĐļĐŊŅ–Đš ĐŗŅ€ŅƒĐŋŅ– Đš ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚ŅŒ Đ´Đž ĐēĐžŅˆĐ¸Đēа Đ˛ŅŅ– Ņ–ĐŊŅˆŅ– Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸.", + "build_image": "ĐžĐąŅ€Đ°Đˇ ĐˇĐąŅ–Ņ€Đēи", + "bulk_delete_duplicates_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŧĐ°ŅĐžĐ˛Đž видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊиК ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}? ĐĻŅ Đ´Ņ–Ņ СаĐģĐ¸ŅˆĐ¸Ņ‚ŅŒ ĐŊĐ°ĐšĐąŅ–ĐģŅŒŅˆĐ¸Đš ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ ҃ ĐēĐžĐļĐŊŅ–Đš ĐŗŅ€ŅƒĐŋŅ– Đš ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐ¸Ņ‚ŅŒ ŅƒŅŅ– Ņ–ĐŊŅˆŅ– Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸. ĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸!", + "bulk_keep_duplicates_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ СаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ {count, plural, one {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊиК ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}? ĐĻĐĩ Đ´Đ°ŅŅ‚ŅŒ СĐŧĐžĐŗŅƒ Đ˛Đ¸Ņ€Ņ–ŅˆĐ¸Ņ‚Đ¸ Đ˛ŅŅ– ĐŗŅ€ŅƒĐŋи Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛ ĐąĐĩС видаĐģĐĩĐŊĐŊŅ ĐąŅƒĐ´ŅŒ-Ņ‡ĐžĐŗĐž.", + "bulk_trash_duplicates_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ Đ´Đž ĐēĐžŅˆĐ¸Đēа {count, plural, one {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊиК ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} 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_title": "Đ”ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ({count})", - "cache_settings_statistics_album": "Đ‘Ņ–ĐąĐģŅ–ĐžŅ‚Đĩ҇ĐŊŅ– ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸", + "cache_settings_duplicated_assets_subtitle": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž, вĐŊĐĩҁĐĩĐŊŅ– ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐēĐžĐŧ Đ´Đž ҁĐŋĐ¸ŅĐē҃ Ņ–ĐŗĐŊĐžŅ€ĐžĐ˛Đ°ĐŊĐ¸Ņ…", + "cache_settings_duplicated_assets_title": "Đ”ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ({count})", + "cache_settings_statistics_album": "ĐœŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", "cache_settings_statistics_full": "ПовĐŊĐžŅ€ĐžĐˇĐŧŅ–Ņ€ĐŊŅ– ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "cache_settings_statistics_shared": "ĐœŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ ҁĐŋŅ–ĐģҌĐŊĐ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛", "cache_settings_statistics_thumbnail": "ĐœŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸", "cache_settings_statistics_title": "ВиĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ ĐēĐĩ҈҃", - "cache_settings_subtitle": "КоĐŊŅ‚Ņ€ĐžĐģŅŽŅ” ĐēĐĩŅˆŅƒĐ˛Đ°ĐŊĐŊŅ ҃ ĐŧĐžĐąŅ–ĐģҌĐŊĐžĐŧ҃ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃", + "cache_settings_subtitle": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐēĐĩŅˆŅƒĐ˛Đ°ĐŊĐŊŅĐŧ ҃ ĐŧĐžĐąŅ–ĐģҌĐŊĐžĐŧ҃ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ Immich", "cache_settings_tile_subtitle": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋОвĐĩĐ´Ņ–ĐŊĐēĐžŅŽ ĐģĐžĐēаĐģҌĐŊĐžĐŗĐž ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", "cache_settings_tile_title": "ЛоĐēаĐģҌĐŊĐĩ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đĩ", "cache_settings_title": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐēĐĩŅˆŅƒĐ˛Đ°ĐŊĐŊŅ", @@ -724,20 +724,20 @@ "cancel_search": "ĐĄĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžŅˆŅƒĐē", "canceled": "ĐĄĐēĐ°ŅĐžĐ˛Đ°ĐŊĐž", "canceling": "ĐĄĐēĐ°ŅŅƒĐ˛Đ°ĐŊĐŊŅ", - "cannot_merge_people": "НĐĩĐŧĐžĐļĐģивО Ой'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš", - "cannot_undo_this_action": "Ви ĐŊĐĩ ĐŧĐžĐļĐĩŅ‚Đĩ ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸ Ņ†ŅŽ Đ´Ņ–ŅŽ!", - "cannot_update_the_description": "НĐĩĐŧĐžĐļĐģивО ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐžĐŋĐ¸Ņ", + "cannot_merge_people": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Ой'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš", + "cannot_undo_this_action": "ĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸!", + "cannot_update_the_description": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐžĐŋĐ¸Ņ", "cast": "ĐĸŅ€Đ°ĐŊҁĐģŅŽĐ˛Đ°Ņ‚Đ¸", - "cast_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°Ņ‚Đ¸ Đ´ĐžŅŅ‚ŅƒĐŋĐŊŅ– ĐŧŅ–ŅŅ†Ņ Ņ‚Ņ€Đ°ĐŊҁĐģŅŅ†Ņ–Ņ—", + "cast_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°Ņ‚Đ¸ Đ´ĐžŅŅ‚ŅƒĐŋĐŊŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— Đ´ĐģŅ Ņ‚Ņ€Đ°ĐŊҁĐģŅŅ†Ņ–Ņ—", "change_date": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ", "change_description": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐžĐŋĐ¸Ņ", "change_display_order": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋĐžŅ€ŅĐ´ĐžĐē Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "change_expiration_time": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Ņ‚ĐĩŅ€ĐŧŅ–ĐŊ Đ´Ņ–Ņ—", - "change_location": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", + "change_location": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆Đĩ", "change_name": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Ņ–Đŧ'Ņ", - "change_name_successfully": "ІĐŧ'Ņ ҃ҁĐŋŅ–ŅˆĐŊĐž СĐŧŅ–ĐŊĐĩĐŊĐž", + "change_name_successfully": "ІĐŧ'Ņ СĐŧŅ–ĐŊĐĩĐŊĐž", "change_password": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", - "change_password_description": "ĐĻĐĩ айО ĐŋĐĩŅ€ŅˆĐ¸Đš Ņ€Đ°Đˇ, ĐēĐžĐģи ви ŅƒĐ˛Ņ–ĐšŅˆĐģи в ŅĐ¸ŅŅ‚ĐĩĐŧ҃, айО ĐąŅƒĐģĐž ĐˇŅ€ĐžĐąĐģĐĩĐŊĐž СаĐŋĐ¸Ņ‚ ĐŊа СĐŧŅ–ĐŊ҃ Đ˛Đ°ŅˆĐžĐŗĐž ĐŋĐ°Ņ€ĐžĐģŅ. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ввĐĩĐ´Ņ–Ņ‚ŅŒ ĐŊОвиК ĐŋĐ°Ņ€ĐžĐģҌ ĐŊиĐļ҇Đĩ.", + "change_password_description": "ĐĻĐĩ Đ˛Đ°Ņˆ ĐŋĐĩŅ€ŅˆĐ¸Đš Đ˛Ņ…Ņ–Đ´ ҃ ŅĐ¸ŅŅ‚ĐĩĐŧ҃ айО ĐąŅƒĐģĐž ĐˇŅ€ĐžĐąĐģĐĩĐŊĐž СаĐŋĐ¸Ņ‚ ĐŊа СĐŧŅ–ĐŊ҃ ĐŋĐ°Ņ€ĐžĐģŅ. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ввĐĩĐ´Ņ–Ņ‚ŅŒ ĐŊОвиК ĐŋĐ°Ņ€ĐžĐģҌ ĐŊиĐļ҇Đĩ.", "change_password_form_confirm_password": "ĐŸŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´Đ¸Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", "change_password_form_description": "ĐŸŅ€Đ¸Đ˛Ņ–Ņ‚, {name},\n\nĐĻĐĩ айО Đ˛Đ°Ņˆ ĐŋĐĩŅ€ŅˆĐ¸Đš Đ˛Ņ…Ņ–Đ´ ҃ ŅĐ¸ŅŅ‚ĐĩĐŧ҃, айО ĐąŅƒĐģĐž ĐŊĐ°Đ´Ņ–ŅĐģаĐŊĐž СаĐŋĐ¸Ņ‚ ĐŊа СĐŧŅ–ĐŊ҃ ĐŋĐ°Ņ€ĐžĐģŅ. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ввĐĩĐ´Ņ–Ņ‚ŅŒ ĐŊОвиК ĐŋĐ°Ņ€ĐžĐģҌ ĐŊиĐļ҇Đĩ.", "change_password_form_log_out": "Đ’Đ¸ĐšŅ‚Đ¸ Ņ–Đˇ ŅĐ¸ŅŅ‚ĐĩĐŧи ĐŊа Đ˛ŅŅ–Ņ… Ņ–ĐŊŅˆĐ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŅ…", @@ -749,93 +749,93 @@ "change_trigger": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Ņ‚Ņ€Đ¸ĐŗĐĩŅ€", "change_trigger_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Ņ‚Ņ€Đ¸ĐŗĐĩŅ€? ĐĻĐĩ видаĐģĐ¸Ņ‚ŅŒ ŅƒŅŅ– ĐŊĐ°ŅĐ˛ĐŊŅ– Đ´Ņ–Ņ— Ņ‚Đ° ҄ҖĐģŅŒŅ‚Ņ€Đ¸.", "change_your_password": "ЗĐŧŅ–ĐŊŅ–Ņ‚ŅŒ ŅĐ˛Ņ–Đš ĐŋĐ°Ņ€ĐžĐģҌ", - "changed_visibility_successfully": "ВидиĐŧŅ–ŅŅ‚ŅŒ ҃ҁĐŋŅ–ŅˆĐŊĐž СĐŧŅ–ĐŊĐĩĐŊĐž", - "charging": "Đ—Đ°Ņ€ŅĐ´Đēа", - "charging_requirement_mobile_backup": "ДĐģŅ Ņ„ĐžĐŊĐžĐ˛ĐžĐŗĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ĐŋŅ€Đ¸ŅŅ‚Ņ€Ņ–Đš ĐŋОвиĐŊĐĩĐŊ ĐˇĐ°Ņ€ŅĐ´ĐļĐ°Ņ‚Đ¸ŅŅ", - "check_corrupt_asset_backup": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đ¸Ņ‚Đ¸ ĐŊа ĐŋĐžŅˆĐēОдĐļĐĩĐŊŅ– Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— Ņ„Đ°ĐšĐģŅ–Đ˛", + "changed_visibility_successfully": "ВидиĐŧŅ–ŅŅ‚ŅŒ СĐŧŅ–ĐŊĐĩĐŊĐž", + "charging": "Đ—Đ°Ņ€ŅĐ´ĐļĐ°Ņ”Ņ‚ŅŒŅŅ", + "charging_requirement_mobile_backup": "ДĐģŅ Ņ„ĐžĐŊĐžĐ˛ĐžĐŗĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ĐŋŅ€Đ¸ŅŅ‚Ņ€Ņ–Đš ĐŧĐ°Ņ” ĐˇĐ°Ņ€ŅĐ´ĐļĐ°Ņ‚Đ¸ŅŅ", + "check_corrupt_asset_backup": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đ¸Ņ‚Đ¸ ĐŊа ĐŋĐžŅˆĐēОдĐļĐĩĐŊŅ– Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", "check_corrupt_asset_backup_button": "ВиĐēĐžĐŊĐ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đē҃", - "check_corrupt_asset_backup_description": "ЗаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ Ņ†ŅŽ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đē҃ ĐģĐ¸ŅˆĐĩ ҇ĐĩŅ€ĐĩС Wi-Fi Ņ‚Đ° ĐŋҖҁĐģŅ Ņ‚ĐžĐŗĐž, ŅĐē Đ˛ŅŅ– Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ СаваĐŊŅ‚Đ°ĐļĐĩĐŊŅ– ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€. ĐŸŅ€ĐžŅ†Đĩҁ ĐŧĐžĐļĐĩ СаКĐŊŅŅ‚Đ¸ ĐēŅ–ĐģҌĐēа Ņ…Đ˛Đ¸ĐģиĐŊ.", + "check_corrupt_asset_backup_description": "ВиĐēĐžĐŊŅƒĐšŅ‚Đĩ Ņ†ŅŽ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đē҃ ĐģĐ¸ŅˆĐĩ ҇ĐĩŅ€ĐĩС Wi-Fi Ņ‚Đ° ĐŋҖҁĐģŅ СавĐĩŅ€ŅˆĐĩĐŊĐŊŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Đ˛ŅŅ–Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛. ĐŸŅ€ĐžŅ†Đĩҁ ĐŧĐžĐļĐĩ СаКĐŊŅŅ‚Đ¸ ĐēŅ–ĐģҌĐēа Ņ…Đ˛Đ¸ĐģиĐŊ.", "check_logs": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đ¸Ņ‚Đ¸ ĐļŅƒŅ€ĐŊаĐģи", "checksum": "КоĐŊŅ‚Ņ€ĐžĐģҌĐŊа ҁ҃Đŧа", "choose_matching_people_to_merge": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐģŅŽĐ´ĐĩĐš Đ´ĐģŅ Ой'Ņ”Đ´ĐŊаĐŊĐŊŅ", "city": "ĐœŅ–ŅŅ‚Đž", - "cleanup_confirm_description": "Immich СĐŊĐ°ĐšŅˆĐžĐ˛ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} (ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐ¸Ņ… Đ´Đž {date}), ĐąĐĩСĐŋĐĩ҇ĐŊĐž СйĐĩŅ€ĐĩĐļĐĩĐŊĐ¸Ņ… ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ. ВидаĐģĐ¸Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– ĐēĐžĐŋŅ–Ņ— С Ņ†ŅŒĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ?", + "cleanup_confirm_description": "Immich СĐŊĐ°ĐšŅˆĐžĐ˛ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} (ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐ¸Ņ… Đ´Đž {date}), ĐąĐĩСĐŋĐĩ҇ĐŊĐž СйĐĩŅ€ĐĩĐļĐĩĐŊĐ¸Ņ… ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ. ВиĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– ĐēĐžĐŋŅ–Ņ— С Ņ†ŅŒĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ?", "cleanup_confirm_prompt_title": "ВиĐģŅƒŅ‡Đ¸Ņ‚Đ¸ С Ņ†ŅŒĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ?", - "cleanup_deleted_assets": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} Đ´Đž ĐēĐžŅˆĐ¸Đēа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", - "cleanup_deleting": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐŊŅ Đ´Đž ĐēĐžŅˆĐ¸Đēа...", - "cleanup_found_assets": "ЗĐŊаКдĐĩĐŊĐž {count} Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐ¸Ņ… ĐēĐžĐŋŅ–Đš Ņ„Đ°ĐšĐģŅ–Đ˛", - "cleanup_found_assets_with_size": "ЗĐŊаКдĐĩĐŊĐž {count} Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐ¸Ņ… ĐēĐžĐŋŅ–Đš Ņ„Đ°ĐšĐģŅ–Đ˛ ({size})", - "cleanup_icloud_shared_albums_excluded": "ĐĄĐŋŅ–ĐģҌĐŊŅ– аĐģŅŒĐąĐžĐŧи iCloud виĐēĐģŅŽŅ‡Đ°ŅŽŅ‚ŅŒŅŅ ĐˇŅ– ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ", - "cleanup_no_assets_found": "НĐĩ СĐŊаКдĐĩĐŊĐž Ņ„Đ°ĐšĐģŅ–Đ˛, Ņ‰Đž Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°ŅŽŅ‚ŅŒ ĐŊавĐĩĐ´ĐĩĐŊиĐŧ Đ˛Đ¸Ņ‰Đĩ ĐēŅ€Đ¸Ņ‚ĐĩŅ€Ņ–ŅĐŧ. Đ¤ŅƒĐŊĐēŅ†Ņ–Ņ ÂĢĐ—Đ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆ĐĩÂģ ĐŧĐžĐļĐĩ видаĐģĐ¸Ņ‚Đ¸ ĐģĐ¸ŅˆĐĩ Ņ„Đ°ĐšĐģи, Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— ŅĐēĐ¸Ņ… ĐąŅƒĐģĐž ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ", - "cleanup_preview_title": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž Đ´ĐģŅ виĐģŅƒŅ‡ĐĩĐŊĐŊŅ ({count})", - "cleanup_step3_description": "ĐĄĐēаĐŊŅƒĐšŅ‚Đĩ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— Ņ„Đ°ĐšĐģŅ–Đ˛, Ņ‰Đž Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°ŅŽŅ‚ŅŒ Đ˛Đ°ŅˆŅ–Đš Đ´Đ°Ņ‚Ņ–, Ņ‚Đ° СйĐĩŅ€ĐĩĐļŅ–Ņ‚ŅŒ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ.", - "cleanup_step4_summary": "{count} Ņ„Đ°ĐšĐģŅ–Đ˛ (ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐ¸Ņ… Đ´Đž {date}) Đ´ĐģŅ видаĐģĐĩĐŊĐŊŅ С Đ˛Đ°ŅˆĐžĐŗĐž ĐģĐžĐēаĐģҌĐŊĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ. Đ¤ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— СаĐģĐ¸ŅˆĐ°Ņ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ Đ´ĐžŅŅ‚ŅƒĐŋĐŊиĐŧи Ņ–Đˇ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ Immich.", - "cleanup_trash_hint": "ЊОй ĐŋОвĐŊŅ–ŅŅ‚ŅŽ ĐˇĐ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆Đĩ Đ´ĐģŅ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ, Đ˛Ņ–Đ´ĐēŅ€Đ¸ĐšŅ‚Đĩ ŅĐ¸ŅŅ‚ĐĩĐŧĐŊ҃ ĐŗĐ°ĐģĐĩŅ€ĐĩŅŽ Ņ‚Đ° ĐžŅ‡Đ¸ŅŅ‚Ņ–Ņ‚ŅŒ ĐēĐžŅˆĐ¸Đē", + "cleanup_deleted_assets": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} Đ´Đž ĐēĐžŅˆĐ¸Đēа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", + "cleanup_deleting": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐŊŅ Đ´Đž ĐēĐžŅˆĐ¸Đēаâ€Ļ", + "cleanup_found_assets": "ЗĐŊаКдĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} Ņ–Đˇ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊиĐŧи ĐēĐžĐŋŅ–ŅĐŧи", + "cleanup_found_assets_with_size": "ЗĐŊаКдĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} Ņ–Đˇ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊиĐŧи ĐēĐžĐŋŅ–ŅĐŧи ({size})", + "cleanup_icloud_shared_albums_excluded": "ĐĄĐŋŅ–ĐģҌĐŊŅ– аĐģŅŒĐąĐžĐŧи iCloud ĐŊĐĩ Đ˛Ņ€Đ°Ņ…ĐžĐ˛ŅƒŅŽŅ‚ŅŒŅŅ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ", + "cleanup_no_assets_found": "НĐĩ СĐŊаКдĐĩĐŊĐž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛, Ņ‰Đž Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°ŅŽŅ‚ŅŒ ĐŊавĐĩĐ´ĐĩĐŊиĐŧ Đ˛Đ¸Ņ‰Đĩ ĐēŅ€Đ¸Ņ‚ĐĩŅ€Ņ–ŅĐŧ. Đ¤ŅƒĐŊĐēŅ†Ņ–Ņ ÂĢĐ—Đ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆ĐĩÂģ ĐŧĐžĐļĐĩ видаĐģĐ¸Ņ‚Đ¸ ĐģĐ¸ŅˆĐĩ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸, Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— ŅĐēĐ¸Ņ… ĐąŅƒĐģĐž ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ", + "cleanup_preview_title": "ЕĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ Đ´Đž виĐģŅƒŅ‡ĐĩĐŊĐŊŅ ({count})", + "cleanup_step3_description": "ĐĄĐēаĐŊŅƒĐ˛Đ°Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´ĐŊĐž Đ´Đž ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ Đ´Đ°Ņ‚Đ¸ Ņ‚Đ° СйĐĩŅ€ĐĩĐļĐĩĐŊĐŊŅ.", + "cleanup_step4_summary": "{count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ (ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊиК Đ´Đž {date})} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ (ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊŅ– Đ´Đž {date})} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ (ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐ¸Ņ… Đ´Đž {date})} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ (ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐ¸Ņ… Đ´Đž {date})}} Đ´ĐģŅ видаĐģĐĩĐŊĐŊŅ С Đ˛Đ°ŅˆĐžĐŗĐž ĐģĐžĐēаĐģҌĐŊĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ. Đ¤ĐžŅ‚Đž СаĐģĐ¸ŅˆĐ°Ņ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ Đ´ĐžŅŅ‚ŅƒĐŋĐŊиĐŧи Ņ–Đˇ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ Immich.", + "cleanup_trash_hint": "ЊОй ĐŋОвĐŊŅ–ŅŅ‚ŅŽ ĐˇĐ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆Đĩ ҃ ŅŅ…ĐžĐ˛Đ¸Ņ‰Ņ–, Đ˛Ņ–Đ´ĐēŅ€Đ¸ĐšŅ‚Đĩ ŅĐ¸ŅŅ‚ĐĩĐŧĐŊ҃ ĐŗĐ°ĐģĐĩŅ€ĐĩŅŽ Ņ‚Đ° ĐžŅ‡Đ¸ŅŅ‚Ņ–Ņ‚ŅŒ ĐēĐžŅˆĐ¸Đē", "clear": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸", "clear_all": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ Đ˛ŅĐĩ", "clear_all_recent_searches": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ Đ˛ŅŅ– ĐžŅŅ‚Đ°ĐŊĐŊŅ– ĐŋĐžŅˆŅƒĐēĐžĐ˛Ņ– СаĐŋĐ¸Ņ‚Đ¸", "clear_file_cache": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐĩ҈ Ņ„Đ°ĐšĐģŅ–Đ˛", - "clear_message": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊĐŊŅ", + "clear_message": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ", "clear_value": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ", "client_cert_dialog_msg_confirm": "ОК", "client_cert_enter_password": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ", - "client_cert_import": "ІĐŧĐŋĐžŅ€Ņ‚", + "client_cert_import": "ІĐŧĐŋĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸", "client_cert_import_success_msg": "КĐģŅ–Ņ”ĐŊŅ‚ŅŅŒĐēиК ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ Ņ–ĐŧĐŋĐžŅ€Ņ‚ĐžĐ˛Đ°ĐŊĐž", "client_cert_invalid_msg": "НĐĩĐ´Ņ–ĐšŅĐŊиК Ņ„Đ°ĐšĐģ ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ° айО ĐŊĐĩĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊиК ĐŋĐ°Ņ€ĐžĐģҌ", "client_cert_password_message": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ Đ´ĐģŅ Ņ†ŅŒĐžĐŗĐž ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ°", "client_cert_password_title": "ĐŸĐ°Ņ€ĐžĐģҌ ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ°", - "client_cert_remove_msg": "КĐģŅ–Ņ”ĐŊŅ‚ŅŅŒĐēиК ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ видаĐģĐĩĐŊĐž", - "client_cert_subtitle": "ĐŸŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ” ĐģĐ¸ŅˆĐĩ Ņ„ĐžŅ€ĐŧĐ°Ņ‚ PKCS12 (.p12, .pfx). ІĐŧĐŋĐžŅ€Ņ‚/видаĐģĐĩĐŊĐŊŅ ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ° Đ´ĐžŅŅ‚ŅƒĐŋĐŊĐĩ ĐģĐ¸ŅˆĐĩ ĐŋĐĩŅ€ĐĩĐ´ Đ˛Ņ…ĐžĐ´ĐžĐŧ ҃ ŅĐ¸ŅŅ‚ĐĩĐŧ҃", - "client_cert_title": "SSL-ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ ĐēĐģŅ–Ņ”ĐŊŅ‚Đ° [ЕКСПЕРИМЕНĐĸАЛĐŦНИЙ]", + "client_cert_remove_msg": "КĐģŅ–Ņ”ĐŊŅ‚ŅŅŒĐēиК ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ виĐģŅƒŅ‡ĐĩĐŊĐž", + "client_cert_subtitle": "ĐŸŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ” ĐģĐ¸ŅˆĐĩ Ņ„ĐžŅ€ĐŧĐ°Ņ‚ PKCS12 (.p12, .pfx). ІĐŧĐŋĐžŅ€Ņ‚/виĐģŅƒŅ‡ĐĩĐŊĐŊŅ ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ° Đ´ĐžŅŅ‚ŅƒĐŋĐŊĐĩ ĐģĐ¸ŅˆĐĩ ĐŋĐĩŅ€ĐĩĐ´ Đ˛Ņ…ĐžĐ´ĐžĐŧ ҃ ŅĐ¸ŅŅ‚ĐĩĐŧ҃", + "client_cert_title": "SSL-ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ ĐēĐģŅ–Ņ”ĐŊŅ‚Đ° [ЕКСПЕРИМЕНĐĸАЛĐŦНО]", "clockwise": "По ĐŗĐžĐ´Đ¸ĐŊĐŊиĐēĐžĐ˛Ņ–Đš ҁ҂ҀҖĐģ҆Җ", "close": "ЗаĐēŅ€Đ¸Ņ‚Đ¸", "collapse": "Đ—ĐŗĐžŅ€ĐŊŅƒŅ‚Đ¸", "collapse_all": "Đ—ĐŗĐžŅ€ĐŊŅƒŅ‚Đ¸ Đ˛ŅĐĩ", "color": "КоĐģŅ–Ņ€", - "color_theme": "КоĐģŅŒĐžŅ€ĐžĐ˛Đ° Ņ‚ĐĩĐŧа", + "color_theme": "ĐĸĐĩĐŧа ĐžŅ„ĐžŅ€ĐŧĐģĐĩĐŊĐŊŅ", "command": "КоĐŧаĐŊда", - "command_palette_prompt": "ШвидĐēĐž СĐŊĐ°Ņ…ĐžĐ´ŅŒŅ‚Đĩ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊ҃ ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃, Đ´Ņ–ŅŽ Ņ‡Đ¸ ĐēĐžĐŧаĐŊĐ´Ņƒ", + "command_palette_prompt": "ШвидĐēиК ĐŋĐžŅˆŅƒĐē ŅŅ‚ĐžŅ€Ņ–ĐŊĐžĐē, Đ´Ņ–Đš Ņ‚Đ° ĐēĐžĐŧаĐŊĐ´", "command_palette_to_close": "СаĐēŅ€Đ¸Ņ‚Đ¸", - "command_palette_to_navigate": "Đ˛Đ˛Ņ–ĐšŅ‚Đ¸", - "command_palette_to_select": "ĐžĐąŅ€Đ°Ņ‚Đ¸", + "command_palette_to_navigate": "ĐŋĐĩŅ€ĐĩĐšŅ‚Đ¸", + "command_palette_to_select": "Đ˛Đ¸ĐąŅ€Đ°Ņ‚Đ¸", "command_palette_to_show_all": "ĐŋĐžĐēĐ°ĐˇĐ°Ņ‚Đ¸ Đ˛ŅĐĩ", "comment_deleted": "КоĐŧĐĩĐŊŅ‚Đ°Ņ€ видаĐģĐĩĐŊĐž", - "comment_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ĐēĐžĐŧĐĩĐŊŅ‚Đ°Ņ€Ņ–Đ˛", + "comment_options": "Đ’Đ°Ņ€Ņ–Đ°ĐŊŅ‚Đ¸ ĐēĐžĐŧĐĩĐŊŅ‚Đ°Ņ€Ņ–Đ˛", "comments_and_likes": "КоĐŧĐĩĐŊŅ‚Đ°Ņ€Ņ– Ņ‚Đ° вĐŋОдОйаĐŊĐŊŅ", "comments_are_disabled": "КоĐŧĐĩĐŊŅ‚Đ°Ņ€Ņ– виĐŧĐēĐŊĐĩĐŊĐž", "common_create_new_album": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊОвиК аĐģŅŒĐąĐžĐŧ", "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-ĐēОд", + "confirm_keep_this_delete_others": "ĐŖŅŅ– Ņ–ĐŊŅˆŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ в ҁ҂ĐĩĐē҃ ĐąŅƒĐ´Đĩ видаĐģĐĩĐŊĐž, ĐžĐēҀҖĐŧ Ņ†ŅŒĐžĐŗĐž. Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŋŅ€ĐžĐ´ĐžĐ˛ĐļĐ¸Ņ‚Đ¸?", + "confirm_new_pin_code": "ĐŸŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´Đ¸Ņ‚Đ¸ ĐŊОвиК PIN-ĐēОд", "confirm_password": "ĐŸŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´Đ¸Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", - "confirm_tag_face": "БаĐļĐ°Ņ”Ņ‚Đĩ ĐŋОСĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ҆Đĩ ОйĐģĐ¸Ņ‡Ņ‡Ņ ŅĐē {name}?", - "confirm_tag_face_unnamed": "БаĐļĐ°Ņ”Ņ‚Đĩ ĐŋОСĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ҆Đĩ ОйĐģĐ¸Ņ‡Ņ‡Ņ?", - "connected_device": "ĐŸŅ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊиК ĐŋŅ€Đ¸ŅŅ‚Ņ€Ņ–Đš", - "connected_to": "ĐŸŅ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊĐž Đ´Đž", - "contain": "ĐœŅ–ŅŅ‚Đ¸Ņ‚Đ¸", + "confirm_tag_face": "ĐĨĐžŅ‡ĐĩŅ‚Đĩ ĐŋОСĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ҆Đĩ ОйĐģĐ¸Ņ‡Ņ‡Ņ ŅĐē {name}?", + "confirm_tag_face_unnamed": "ĐĨĐžŅ‡ĐĩŅ‚Đĩ ĐŋОСĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ҆Đĩ ОйĐģĐ¸Ņ‡Ņ‡Ņ?", + "connected_device": "ĐŸŅ–Đ´'Ņ”Đ´ĐŊаĐŊиК ĐŋŅ€Đ¸ŅŅ‚Ņ€Ņ–Đš", + "connected_to": "ĐŸŅ–Đ´'Ņ”Đ´ĐŊаĐŊĐž Đ´Đž", + "contain": "ВĐŋĐ¸ŅĐ°Ņ‚Đ¸", "context": "КоĐŊŅ‚ĐĩĐēҁ҂", "continue": "ĐŸŅ€ĐžĐ´ĐžĐ˛ĐļĐ¸Ņ‚Đ¸", "control_bottom_app_bar_create_new_album": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊОвиК аĐģŅŒĐąĐžĐŧ", "control_bottom_app_bar_delete_from_immich": "ВидаĐģĐ¸Ņ‚Đ¸ С Immich", "control_bottom_app_bar_delete_from_local": "ВидаĐģĐ¸Ņ‚Đ¸ С ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", - "control_bottom_app_bar_edit_location": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", + "control_bottom_app_bar_edit_location": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧҖҁ҆Đĩ", "control_bottom_app_bar_edit_time": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ Ņ‚Đ° Ņ‡Đ°Ņ", - "control_bottom_app_bar_share_link": "ĐŸĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ", + "control_bottom_app_bar_share_link": "ĐŸĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧ", "control_bottom_app_bar_share_to": "ĐŸĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ", - "control_bottom_app_bar_trash_from_immich": "До ĐēĐžŅˆĐ¸Đēа", - "copied_image_to_clipboard": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ҁĐēĐžĐŋŅ–ĐšĐžĐ˛Đ°ĐŊĐž в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧŅ–ĐŊ҃.", - "copied_to_clipboard": "ĐĄĐēĐžĐŋŅ–ĐšĐžĐ˛Đ°ĐŊĐž в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧŅ–ĐŊ҃!", - "copy_error": "ПоĐŧиĐģĐēа ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", + "control_bottom_app_bar_trash_from_immich": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ Đ´Đž ĐēĐžŅˆĐ¸Đēа", + "copied_image_to_clipboard": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ҁĐēĐžĐŋŅ–ĐšĐžĐ˛Đ°ĐŊĐž Đ´Đž ĐąŅƒŅ„ĐĩŅ€Đ° ОйĐŧŅ–ĐŊ҃.", + "copied_to_clipboard": "ĐĄĐēĐžĐŋŅ–ĐšĐžĐ˛Đ°ĐŊĐž Đ´Đž ĐąŅƒŅ„ĐĩŅ€Đ° ОйĐŧŅ–ĐŊ҃!", + "copy_error": "НĐĩ вдаĐģĐžŅŅ ҁĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸", "copy_file_path": "ĐĄĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ ҈ĐģŅŅ… Đ´Đž Ņ„Đ°ĐšĐģ҃", "copy_image": "ĐĄĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "copy_link": "ĐĄĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", - "copy_link_to_clipboard": "ĐĄĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧŅ–ĐŊ҃", + "copy_link_to_clipboard": "ĐĄĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ Đ´Đž ĐąŅƒŅ„ĐĩŅ€Đ° ОйĐŧŅ–ĐŊ҃", "copy_password": "ĐĄĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", - "copy_to_clipboard": "ĐĄĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧŅ–ĐŊ҃", + "copy_to_clipboard": "ĐĄĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ Đ´Đž ĐąŅƒŅ„ĐĩŅ€Đ° ОйĐŧŅ–ĐŊ҃", "country": "ĐšŅ€Đ°Ņ—ĐŊа", "cover": "ОбĐēĐģадиĐŊĐēа", "covers": "ОбĐēĐģадиĐŊĐēи", @@ -843,90 +843,92 @@ "create_album": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", "create_album_page_untitled": "БĐĩС ĐŊаСви", "create_api_key": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡ API", - "create_first_workflow": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŋĐĩŅ€ŅˆĐ¸Đš Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", + "create_first_workflow": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŋĐĩŅ€ŅˆŅƒ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–ŅŽ", "create_library": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", "create_link": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "create_link_to_share": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž Đ´ĐžŅŅ‚ŅƒĐŋ҃", - "create_link_to_share_description": "ДозвоĐģĐ¸Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš Са ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧ ĐąŅƒĐ´ŅŒ-ĐēĐžĐŧ҃", + "create_link_to_share_description": "Đ”Đ°Ņ‚Đ¸ СĐŧĐžĐŗŅƒ ĐąŅƒĐ´ŅŒ-ĐēĐžĐŧ҃ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‚Đ¸ Đ˛Đ¸ĐąŅ€Đ°ĐŊŅ– Ņ„ĐžŅ‚Đž Са ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧ", "create_new": "ĐĄĐĸВОРИĐĸИ НОВИЙ", - "create_new_person": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊĐžĐ˛Ņƒ ĐžŅĐžĐąŅƒ", - "create_new_person_hint": "ĐŸŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐžĐąŅ€Đ°ĐŊиĐŧ Ņ„ĐžŅ‚Đž ĐŊĐžĐ˛Ņƒ ĐžŅĐžĐąŅƒ", + "create_new_face": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊОвĐĩ ОйĐģĐ¸Ņ‡Ņ‡Ņ", + "create_new_person": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊĐžĐ˛Ņƒ ĐģŅŽĐ´Đ¸ĐŊ҃", + "create_new_person_hint": "ĐŸŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ Đ˛Đ¸ĐąŅ€Đ°ĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŊĐžĐ˛Ņ–Đš ĐģŅŽĐ´Đ¸ĐŊŅ–", "create_new_user": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊĐžĐ˛ĐžĐŗĐž ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "create_shared_album_page_share_add_assets": "ДОДАĐĸИ ФОĐĸО/ВІДЕО", + "create_person": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐģŅŽĐ´Đ¸ĐŊ҃", + "create_person_subtitle": "Đ”ĐžĐ´Đ°ĐšŅ‚Đĩ Ņ–Đŧ'Ņ Đ´Đž Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž ОйĐģĐ¸Ņ‡Ņ‡Ņ, Ņ‰ĐžĐą ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Ņ‚Đ° ĐŋОСĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐŊĐžĐ˛Ņƒ ĐžŅĐžĐąŅƒ", + "create_shared_album_page_share_add_assets": "ДОДАĐĸИ ЕЛЕМЕНĐĸИ", "create_shared_album_page_share_select_photos": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž", "create_shared_link": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "create_tag": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Ņ‚ĐĩĐŗ", - "create_tag_description": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊОвиК Ņ‚ĐĩĐŗ. ДĐģŅ вĐēĐģадĐĩĐŊĐ¸Ņ… Ņ‚ĐĩĐŗŅ–Đ˛ вĐēаĐļŅ–Ņ‚ŅŒ ĐŋОвĐŊиК ҈ĐģŅŅ… Ņ‚ĐĩĐŗĐ°, вĐēĐģŅŽŅ‡Đ°ŅŽŅ‡Đ¸ ҁĐģĐĩŅˆŅ–.", + "create_tag_description": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊОвиК Ņ‚ĐĩĐŗ. ДĐģŅ вĐēĐģадĐĩĐŊĐ¸Ņ… Ņ‚ĐĩĐŗŅ–Đ˛ вĐēаĐļŅ–Ņ‚ŅŒ ĐŋОвĐŊиК ҈ĐģŅŅ… Ņ‚ĐĩĐŗĐ°, Ņ€Đ°ĐˇĐžĐŧ ĐˇŅ– ҁĐēҖҁĐŊĐžŅŽ Ņ€Đ¸ŅĐēĐžŅŽ (/).", "create_user": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "create_workflow": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", + "create_workflow": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–ŅŽ", "created": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž", "created_at": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž", - "creating_linked_albums": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŋĐžĐ˛â€™ŅĐˇĐ°ĐŊĐ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛...", + "creating_linked_albums": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŋĐžĐ˛â€™ŅĐˇĐ°ĐŊĐ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛â€Ļ", "crop": "ĐšĐ°Đ´Ņ€ŅƒĐ˛Đ°Ņ‚Đ¸", "crop_aspect_ratio_fixed": "Đ¤Ņ–ĐēŅĐžĐ˛Đ°ĐŊĐĩ", "crop_aspect_ratio_free": "Đ’Ņ–ĐģҌĐŊĐĩ", - "crop_aspect_ratio_original": "ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ", + "crop_aspect_ratio_original": "ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐĩ", + "crop_aspect_ratio_square": "ĐšĐ˛Đ°Đ´Ņ€Đ°Ņ‚ĐŊĐĩ", "curated_object_page_title": "Đ Đĩ҇Җ", "current_device": "ĐŸĐžŅ‚ĐžŅ‡ĐŊиК ĐŋŅ€Đ¸ŅŅ‚Ņ€Ņ–Đš", "current_pin_code": "ĐŸĐžŅ‚ĐžŅ‡ĐŊиК PIN-ĐēОд", "current_server_address": "ĐŸĐžŅ‚ĐžŅ‡ĐŊа Đ°Đ´Ņ€ĐĩŅĐ° ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", - "custom_date": "ВĐģĐ°ŅĐŊа Đ´Đ°Ņ‚Đ°", - "custom_locale": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ†ŅŒĐēиК Ņ€ĐĩĐŗŅ–ĐžĐŊ", - "custom_locale_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ, Ņ‡Đ°Ņ Ņ‚Đ° Ņ‡Đ¸ŅĐģа С ŅƒŅ€Đ°Ņ…ŅƒĐ˛Đ°ĐŊĐŊŅĐŧ ĐžĐąŅ€Đ°ĐŊĐžŅ— ĐŧОви Ņ‚Đ° Ņ€ĐĩĐŗŅ–ĐžĐŊ҃", - "custom_url": "ВĐģĐ°ŅĐŊа URL-Đ°Đ´Ņ€ĐĩŅĐ°", - "cutoff_date_description": "ЗбĐĩŅ€ĐĩĐļŅ–Ņ‚ŅŒ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— С ĐžŅŅ‚Đ°ĐŊĐŊŅŒĐžĐŗĐžâ€Ļ", + "custom_date": "Đ”ĐžĐ˛Ņ–ĐģҌĐŊа Đ´Đ°Ņ‚Đ°", + "custom_locale": "Đ”ĐžĐ˛Ņ–ĐģҌĐŊŅ– Ņ€ĐĩĐŗŅ–ĐžĐŊаĐģҌĐŊŅ– ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", + "custom_locale_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ, Ņ‡Đ°Ņ Ņ‚Đ° Ņ‡Đ¸ŅĐģа С ŅƒŅ€Đ°Ņ…ŅƒĐ˛Đ°ĐŊĐŊŅĐŧ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžŅ— ĐŧОви Ņ‚Đ° Ņ€ĐĩĐŗŅ–ĐžĐŊ҃", + "custom_url": "Đ”ĐžĐ˛Ņ–ĐģҌĐŊа URL-Đ°Đ´Ņ€ĐĩŅĐ°", + "cutoff_date_description": "ЗбĐĩŅ€Ņ–ĐŗĐ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž Са ĐžŅŅ‚Đ°ĐŊĐŊŅ–â€Ļ", "cutoff_day": "{count, plural, one {Đ´ĐĩĐŊҌ} few {Đ´ĐŊŅ–} many {Đ´ĐŊŅ–Đ˛} other {Đ´ĐŊŅ–Đ˛}}", "cutoff_year": "{count, plural, one {ҀҖĐē} few {Ņ€ĐžĐēи} many {Ņ€ĐžĐēŅ–Đ˛} other {Ņ€ĐžĐēŅ–Đ˛}}", - "daily_title_text_date": "Е, МММ Đ´Đ´", - "daily_title_text_date_year": "Е, МММ Đ´Đ´, ҀҀҀҀ", + "daily_title_text_date": "E, dd MMM", + "daily_title_text_date_year": "E, dd MMM yyyy", "dark": "ĐĸĐĩĐŧĐŊа", - "dark_theme": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ‚ĐĩĐŧĐŊ҃ Ņ‚ĐĩĐŧ҃", + "dark_theme": "ПĐĩŅ€ĐĩĐŧĐēĐŊŅƒŅ‚Đ¸ ĐŊа Ņ‚ĐĩĐŧĐŊ҃ Ņ‚ĐĩĐŧ҃", "date": "Đ”Đ°Ņ‚Đ°", "date_after": "Đ”Đ°Ņ‚Đ° ĐŋҖҁĐģŅ", - "date_and_time": "Đ”Đ°Ņ‚Đ° Ņ– Ņ‡Đ°Ņ", + "date_and_time": "Đ”Đ°Ņ‚Đ° Đš Ņ‡Đ°Ņ", "date_before": "Đ”Đ°Ņ‚Đ° Đ´Đž", - "date_format": "Е, ЛЛЛ Đ´, Ņ€ â€ĸ Đŗ:ĐŧĐŧ дĐŋ", - "date_of_birth_saved": "Đ”Đ°Ņ‚Đ° ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ ҃ҁĐŋŅ–ŅˆĐŊĐž СйĐĩŅ€ĐĩĐļĐĩĐŊа", - "date_range": "ĐŸŅ€ĐžĐŧŅ–ĐļĐžĐē Ņ‡Đ°ŅŅƒ", + "date_format": "E, d LLL y â€ĸ HH:mm", + "date_of_birth_saved": "Đ”Đ°Ņ‚Ņƒ ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ СйĐĩŅ€ĐĩĐļĐĩĐŊĐž", + "date_range": "Đ”Ņ–Đ°ĐŋаСОĐŊ Đ´Đ°Ņ‚", "day": "ДĐĩĐŊҌ", "days": "ДĐŊŅ–", "deduplicate_all": "ВидаĐģĐ¸Ņ‚Đ¸ Đ˛ŅŅ– Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸", - "deduplication_criteria_1": "РОСĐŧŅ–Ņ€ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ в ĐąĐ°ĐšŅ‚Đ°Ņ…", - "deduplication_criteria_2": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ даĐŊĐ¸Ņ… EXIF", - "deduplication_info": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž Đ´ĐĩĐ´ŅƒĐŋĐģŅ–ĐēĐ°Ņ†Ņ–ŅŽ", - "deduplication_info_description": "ДĐģŅ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐžĐŗĐž ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž Đ˛Đ¸ĐąĐžŅ€Ņƒ Ņ„Đ°ĐšĐģŅ–Đ˛ Ņ– ĐŧĐ°ŅĐžĐ˛ĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛ Đŧи Đ˛Ņ€Đ°Ņ…ĐžĐ˛ŅƒŅ”ĐŧĐž:", + "default_locale": "ĐĸиĐŋОва ĐģĐžĐēаĐģҌ", + "default_locale_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Đ¸ Ņ‚Đ° Ņ‡Đ¸ŅĐģа Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´ĐŊĐž Đ´Đž ĐģĐžĐēаĐģŅ– Đ˛Đ°ŅˆĐžĐŗĐž ĐąŅ€Đ°ŅƒĐˇĐĩŅ€Đ°", "delete": "ВидаĐģĐ¸Ņ‚Đ¸", - "delete_action_confirmation_message": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ҆ĐĩĐš Ņ„Đ°ĐšĐģ? Đ™ĐžĐŗĐž ĐąŅƒĐ´Đĩ ĐŋĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ, а Ņ‚Đ°ĐēĐžĐļ СĘŧŅĐ˛Đ¸Ņ‚ŅŒŅŅ СаĐŋĐ¸Ņ‚ ĐŊа ĐšĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ С ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", - "delete_action_prompt": "ВидаĐģĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "delete_action_confirmation_message": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ҆ĐĩĐš ĐĩĐģĐĩĐŧĐĩĐŊŅ‚? Đ™ĐžĐŗĐž ĐąŅƒĐ´Đĩ ĐŋĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ, а Ņ‚Đ°ĐēĐžĐļ С'ŅĐ˛Đ¸Ņ‚ŅŒŅŅ СаĐŋĐ¸Ņ‚ ĐŊа ĐšĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ С ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", + "delete_action_prompt": "ВидаĐģĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", "delete_album": "ВидаĐģĐ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", "delete_api_key_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ҆ĐĩĐš ĐēĐģŅŽŅ‡ API?", - "delete_dialog_alert": "ĐĻŅ– Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊŅ– С ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ Immich Ņ‚Đ° Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", - "delete_dialog_alert_local": "ĐĻŅ– Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊŅ– С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ, аĐģĐĩ СаĐģĐ¸ŅˆĐ°Ņ‚ŅŒŅŅ Đ´ĐžŅŅ‚ŅƒĐŋĐŊиĐŧи ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ Immich", - "delete_dialog_alert_local_non_backed_up": "ДĐĩŅĐēŅ– Ņ„Đ°ĐšĐģи ĐŊĐĩ ĐąŅƒĐģи СйĐĩŅ€ĐĩĐļĐĩĐŊŅ– ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ Immich Ņ– ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊŅ– С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", - "delete_dialog_alert_remote": "ĐĻŅ– Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐŊаСавĐļди видаĐģĐĩĐŊŅ– С ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ Immich", + "delete_dialog_alert": "ĐĻŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐąŅƒĐ´Đĩ ĐŊаСавĐļди видаĐģĐĩĐŊĐž С Immich Ņ‚Đ° С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", + "delete_dialog_alert_local": "ĐĻŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐąŅƒĐ´Đĩ ĐŊаСавĐļди видаĐģĐĩĐŊĐž С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ, аĐģĐĩ вОĐŊи СаĐģĐ¸ŅˆĐ°Ņ‚ŅŒŅŅ ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ Immich", + "delete_dialog_alert_local_non_backed_up": "ДĐĩŅĐēŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŊĐĩ СйĐĩŅ€ĐĩĐļĐĩĐŊĐž ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ Immich, Ņ– Ņ—Ņ… ĐąŅƒĐ´Đĩ ĐŊаСавĐļди видаĐģĐĩĐŊĐž С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", + "delete_dialog_alert_remote": "ĐĻŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐąŅƒĐ´Đĩ ĐŊаСавĐļди видаĐģĐĩĐŊĐž С ҁĐĩŅ€Đ˛ĐĩŅ€Đ° Immich", "delete_dialog_ok_force": "Đ’ŅĐĩ ОдĐŊĐž видаĐģĐ¸Ņ‚Đ¸", - "delete_dialog_title": "ВидаĐģĐ¸Ņ‚Đ¸ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž", + "delete_dialog_title": "ВидаĐģĐ¸Ņ‚Đ¸ ĐŊаСавĐļди", "delete_duplicates_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŊаСавĐļди видаĐģĐ¸Ņ‚Đ¸ ҆Җ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸?", "delete_face": "ВидаĐģĐ¸Ņ‚Đ¸ ОйĐģĐ¸Ņ‡Ņ‡Ņ", "delete_key": "ВидаĐģĐ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡", "delete_library": "ВидаĐģĐ¸Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", "delete_link": "ВидаĐģĐ¸Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", - "delete_local_action_prompt": "ВидаĐģĐĩĐŊĐž С ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", - "delete_local_dialog_ok_backed_up_only": "ВидаĐģĐ¸Ņ‚Đ¸ ĐģĐ¸ŅˆĐĩ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ—", + "delete_local_action_prompt": "ВидаĐģĐĩĐŊĐž С ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "delete_local_dialog_ok_backed_up_only": "ВидаĐģĐ¸Ņ‚Đ¸ ĐģĐ¸ŅˆĐĩ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸, СйĐĩŅ€ĐĩĐļĐĩĐŊŅ– ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ", "delete_local_dialog_ok_force": "Đ’ŅĐĩ ОдĐŊĐž видаĐģĐ¸Ņ‚Đ¸", "delete_others": "ВидаĐģĐ¸Ņ‚Đ¸ Ņ–ĐŊŅˆŅ–", "delete_permanently": "ВидаĐģĐ¸Ņ‚Đ¸ ĐŊаСавĐļди", - "delete_permanently_action_prompt": "ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "delete_permanently_action_prompt": "ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", "delete_shared_link": "ВидаĐģĐ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "delete_shared_link_dialog_title": "ВидаĐģĐ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", - "delete_tag": "ВидаĐģĐ¸Ņ‚Đ¸ ĐĸĐĩĐŗ", + "delete_tag": "ВидаĐģĐ¸Ņ‚Đ¸ Ņ‚ĐĩĐŗ", "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": "ПоĐŧиĐģĐēа ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐžĐŋĐ¸ŅŅƒ, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ ĐļŅƒŅ€ĐŊаĐģ Đ´ĐģŅ ĐŋĐžĐ´Ņ€ĐžĐąĐ¸Ņ†ŅŒ", + "description_input_hint_text": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐžĐŋĐ¸Ņâ€Ļ", + "description_input_submit_error": "НĐĩ вдаĐģĐžŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐžĐŋĐ¸Ņ. ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅŒŅ‚Đĩ ĐļŅƒŅ€ĐŊаĐģ Đ´ĐģŅ Đ´ĐĩŅ‚Đ°ĐģĐĩĐš", "deselect_all": "ĐĄĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛Đ¸ĐąŅ–Ņ€ ŅƒŅŅ–Ņ…", "details": "ДĐĩŅ‚Đ°ĐģŅ–", "direction": "НаĐŋŅ€ŅĐŧ", @@ -934,77 +936,77 @@ "disabled": "ВиĐŧĐēĐŊĐĩĐŊĐž", "disallow_edits": "Đ—Đ°ĐąĐžŅ€ĐžĐŊĐ¸Ņ‚Đ¸ Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°ĐŊĐŊŅ", "discord": "Discord", - "discover": "Đ’Đ¸ŅĐ˛Đ¸Ņ‚Đ¸", + "discover": "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Ņ‚Ņ", "discovered_devices": "Đ’Đ¸ŅĐ˛ĐģĐĩĐŊŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", - "dismiss_all_errors": "ĐŸŅ€ĐžĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ Đ˛ŅŅ– ĐŋĐžĐŧиĐģĐēи", - "dismiss_error": "ĐŸŅ€ĐžĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ ĐŋĐžĐŧиĐģĐē҃", - "display_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", + "dismiss_all_errors": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ Đ˛ŅŅ– ĐŋĐžĐŧиĐģĐēи", + "dismiss_error": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐŋĐžĐŧиĐģĐē҃", + "display_options": "Đ’Đ°Ņ€Ņ–Đ°ĐŊŅ‚Đ¸ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "display_order": "ĐŸĐžŅ€ŅĐ´ĐžĐē Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", - "display_original_photos": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐ¸Ņ… Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš", - "display_original_photos_setting_description": "ĐĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅŽ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐžĐŗĐž Ņ„ĐžŅ‚Đž ĐŋŅ€Đ¸ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—, ŅĐēŅ‰Đž ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐĩ Ņ„ĐžŅ‚Đž ҁ҃ĐŧҖҁĐŊĐĩ С вĐĩйОĐŧ. ĐĻĐĩ ĐŧĐžĐļĐĩ ĐŋŅ€Đ¸ĐˇĐ˛ĐĩŅŅ‚Đ¸ Đ´Đž ĐŋĐžĐ˛Ņ–ĐģҌĐŊŅ–ŅˆĐžĐŗĐž Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš.", - "do_not_show_again": "НĐĩ ĐŋĐžĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ ҆Đĩ ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊĐŊŅ СĐŊĐžĐ˛Ņƒ", + "display_original_photos": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊŅ– Ņ„ĐžŅ‚Đž", + "display_original_photos_setting_description": "ПоĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ СаĐŧŅ–ŅŅ‚ŅŒ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸, ŅĐēŅ‰Đž Ņ„ĐžŅ€ĐŧĐ°Ņ‚ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ° ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ”Ņ‚ŅŒŅŅ ĐąŅ€Đ°ŅƒĐˇĐĩŅ€ĐžĐŧ. ĐĻĐĩ ĐŧĐžĐļĐĩ ҁĐŋĐžĐ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ.", + "do_not_show_again": "Đ‘Ņ–ĐģҌ҈Đĩ ĐŊĐĩ ĐŋĐžĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ ҆Đĩ ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊĐŊŅ", "documentation": "ДоĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ–Ņ", "done": "Đ“ĐžŅ‚ĐžĐ˛Đž", "download": "ЗаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸", - "download_action_prompt": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ {count} Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", + "download_action_prompt": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", "download_canceled": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ҁĐēĐ°ŅĐžĐ˛Đ°ĐŊĐž", "download_complete": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ СаĐēŅ–ĐŊ҇ĐĩĐŊĐž", "download_enqueue": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŋĐžŅŅ‚Đ°Đ˛ĐģĐĩĐŊĐž в ҇ĐĩŅ€ĐŗŅƒ", - "download_error": "ПоĐŧиĐģĐēа СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", - "download_failed": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŊĐĩ вдаĐģĐžŅŅ", + "download_error": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸", + "download_failed": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸", "download_finished": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ СаĐēŅ–ĐŊ҇ĐĩĐŊĐž", "download_include_embedded_motion_videos": "Đ’ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊŅ– Đ˛Ņ–Đ´ĐĩĐž", - "download_include_embedded_motion_videos_description": "ВĐēĐģŅŽŅ‡Đ°Ņ‚Đ¸ Đ˛Ņ–Đ´ĐĩĐž, Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊŅ– в Ņ€ŅƒŅ…ĐžĐŧŅ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—, ŅĐē ĐžĐēŅ€ĐĩĐŧĐĩ Đ˛Ņ–Đ´ĐĩĐž", - "download_notfound": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŊĐĩ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐž", + "download_include_embedded_motion_videos_description": "Đ”ĐžĐ´Đ°Đ˛Đ°Ņ‚Đ¸ Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊŅ– Đ˛Ņ–Đ´ĐĩĐž С Ņ€ŅƒŅ…ĐžĐŧĐ¸Ņ… Ņ„ĐžŅ‚Đž ŅĐē ĐžĐēŅ€ĐĩĐŧиК Ņ„Đ°ĐšĐģ", + "download_notfound": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž", "download_original": "ЗаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ", "download_paused": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŋŅ€Đ¸ĐˇŅƒĐŋиĐŊĐĩĐŊĐž", - "download_settings": "ЗаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸", - "download_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи, ĐŋОв'ŅĐˇĐ°ĐŊиĐŧи С СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅĐŧ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", + "download_settings": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "download_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", "download_started": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ Ņ€ĐžĐˇĐŋĐžŅ‡Đ°Ņ‚Đž", - "download_sucess": "ĐŖŅĐŋŅ–ŅˆĐŊĐĩ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", - "download_sucess_android": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž в DCIM/Immich", + "download_sucess": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž", + "download_sucess_android": "МĐĩĐ´Ņ–Đ° СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž в DCIM/Immich", "download_waiting_to_retry": "ĐžŅ‡Ņ–ĐēŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐžŅ— ҁĐŋŅ€ĐžĐąĐ¸", "downloading": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", - "downloading_asset_filename": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ Ņ„Đ°ĐšĐģ҃ {filename}", + "downloading_asset_filename": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ° {filename}", "downloading_from_icloud": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ С iCloud", "downloading_media": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŧĐĩĐ´Ņ–Đ°", - "drop_files_to_upload": "ПĐĩŅ€ĐĩĐŊĐĩŅŅ–Ņ‚ŅŒ Ņ„Đ°ĐšĐģи в ĐąŅƒĐ´ŅŒ-ŅĐēĐĩ ĐŧҖҁ҆Đĩ Đ´ĐģŅ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "drop_files_to_upload": "ПĐĩŅ€ĐĩŅ‚ŅĐŗĐŊŅ–Ņ‚ŅŒ Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅŒ-ĐēŅƒĐ´Đ¸, Ņ‰ĐžĐą виваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸", "duplicates": "Đ”ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸", - "duplicates_description": "ВизĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸, ŅĐēŅ– ĐŗŅ€ŅƒĐŋи Ņ” Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ°Đŧи", + "duplicates_description": "ОĐŋŅ€Đ°Ņ†ŅŽĐšŅ‚Đĩ ĐēĐžĐļĐŊ҃ ĐŗŅ€ŅƒĐŋ҃, вĐēĐ°ĐˇĐ°Đ˛ŅˆĐ¸, ŅĐēŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸, ŅĐēŅ‰Đž Ņ‚Đ°ĐēŅ– Ņ”, Ņ” Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ°Đŧи", "duration": "ĐĸŅ€Đ¸Đ˛Đ°ĐģŅ–ŅŅ‚ŅŒ", - "edit": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸", + "edit": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸", "edit_album": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", "edit_avatar": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Đ°Đ˛Đ°Ņ‚Đ°Ņ€", "edit_birthday": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ", "edit_date": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ", "edit_date_and_time": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ Ņ‚Đ° Ņ‡Đ°Ņ", - "edit_date_and_time_action_prompt": "ЗĐŧŅ–ĐŊĐĩĐŊĐž Đ´Đ°Ņ‚Ņƒ Ņ‚Đ° Ņ‡Đ°Ņ ҃ {count, plural, one {# Ņ„Đ°ĐšĐģŅ–} few {# Ņ„Đ°ĐšĐģĐ°Ņ…} other {# Ņ„Đ°ĐšĐģĐ°Ņ…}}", - "edit_date_and_time_by_offset": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ Са СĐŧҖ҉ĐĩĐŊĐŊŅĐŧ", + "edit_date_and_time_action_prompt": "ЗĐŧŅ–ĐŊĐĩĐŊĐž Đ´Đ°Ņ‚Ņƒ Ņ‚Đ° Ņ‡Đ°Ņ ҃ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊ҂Җ} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°Ņ…} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°Ņ…} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°Ņ…}}", + "edit_date_and_time_by_offset": "ЗĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ Са ĐˇŅŅƒĐ˛ĐžĐŧ", "edit_date_and_time_by_offset_interval": "Новий Đ´Ņ–Đ°ĐŋаСОĐŊ Đ´Đ°Ņ‚: {from} - {to}", "edit_description": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐžĐŋĐ¸Ņ", - "edit_description_prompt": "Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, вийĐĩŅ€Ņ–Ņ‚ŅŒ ĐŊОвиК ĐžĐŋĐ¸Ņ:", + "edit_description_prompt": "ОбĐĩŅ€Ņ–Ņ‚ŅŒ ĐŊОвиК ĐžĐŋĐ¸Ņ:", "edit_exclusion_pattern": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ виĐēĐģŅŽŅ‡ĐĩĐŊҌ", - "edit_faces": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°ĐŊĐŊŅ ОйĐģĐ¸Ņ‡", - "edit_key": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡", + "edit_faces": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ОйĐģĐ¸Ņ‡Ņ‡Ņ", + "edit_key": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐēĐģŅŽŅ‡", "edit_link": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", - "edit_location": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", - "edit_location_action_prompt": "ЗĐŧŅ–ĐŊĐĩĐŊĐž ĐŧŅ–ŅŅ†ŅŒ СКОĐŧĐēи: {count}", - "edit_location_dialog_title": "ĐœŅ–ŅŅ†ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", - "edit_name": "Đ’Ņ–Đ´Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Ņ–Đŧ'Ņ", + "edit_location": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧҖҁ҆Đĩ", + "edit_location_action_prompt": "ЗĐŧŅ–ĐŊĐĩĐŊĐž ĐŧҖҁ҆Đĩ СКОĐŧĐēи Đ´ĐģŅ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "edit_location_dialog_title": "ĐœŅ–ŅŅ†Đĩ", + "edit_name": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Ņ–Đŧ'Ņ", "edit_people": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš", "edit_tag": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Ņ‚ĐĩĐŗ", "edit_title": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐˇĐ°ĐŗĐžĐģОвОĐē", "edit_user": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "edit_workflow": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", + "edit_workflow": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–ŅŽ", "editor": "Đ ĐĩдаĐēŅ‚ĐžŅ€", - "editor_close_without_save_prompt": "ЗĐŧŅ–ĐŊи ĐŊĐĩ ĐąŅƒĐ´ŅƒŅ‚ŅŒ СйĐĩŅ€ĐĩĐļĐĩĐŊŅ–", + "editor_close_without_save_prompt": "ЗĐŧŅ–ĐŊи ĐŊĐĩ ĐąŅƒĐ´Đĩ СйĐĩŅ€ĐĩĐļĐĩĐŊĐž", "editor_close_without_save_title": "ЗаĐēŅ€Đ¸Ņ‚Đ¸ Ņ€ĐĩдаĐēŅ‚ĐžŅ€?", "editor_confirm_reset_all_changes": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ҁĐēиĐŊŅƒŅ‚Đ¸ Đ˛ŅŅ– СĐŧŅ–ĐŊи?", "editor_discard_edits_confirm": "ĐĄĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸ СĐŧŅ–ĐŊи", "editor_discard_edits_prompt": "ĐŖ Đ˛Đ°Ņ Ņ” ĐŊĐĩСйĐĩŅ€ĐĩĐļĐĩĐŊŅ– СĐŧŅ–ĐŊи. Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Ņ—Ņ… ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸?", "editor_discard_edits_title": "ĐĄĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸ СĐŧŅ–ĐŊи?", "editor_edits_applied_error": "НĐĩ вдаĐģĐžŅŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ СĐŧŅ–ĐŊи", - "editor_edits_applied_success": "ЗĐŧŅ–ĐŊи ҃ҁĐŋŅ–ŅˆĐŊĐž ĐˇĐ°ŅŅ‚ĐžŅĐžĐ˛Đ°ĐŊĐž", + "editor_edits_applied_success": "ЗĐŧŅ–ĐŊи ĐˇĐ°ŅŅ‚ĐžŅĐžĐ˛Đ°ĐŊĐž", "editor_flip_horizontal": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐˇĐ¸Ņ‚Đ¸ ĐŗĐžŅ€Đ¸ĐˇĐžĐŊŅ‚Đ°ĐģҌĐŊĐž", "editor_flip_vertical": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐˇĐ¸Ņ‚Đ¸ вĐĩŅ€Ņ‚Đ¸ĐēаĐģҌĐŊĐž", "editor_handle_corner": "{corner, select, top_left {Đ›Ņ–Đ˛Đ¸Đš вĐĩҀ҅ĐŊŅ–Đš ĐēŅƒŅ‚} top_right {ĐŸŅ€Đ°Đ˛Đ¸Đš вĐĩҀ҅ĐŊŅ–Đš ĐēŅƒŅ‚} bottom_left {Đ›Ņ–Đ˛Đ¸Đš ĐŊиĐļĐŊŅ–Đš ĐēŅƒŅ‚} bottom_right {ĐŸŅ€Đ°Đ˛Đ¸Đš ĐŊиĐļĐŊŅ–Đš ĐēŅƒŅ‚} other {ĐšŅƒŅ‚}}", @@ -1017,158 +1019,158 @@ "email_notifications": "ĐĄĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ ĐĩĐģ. ĐŋĐžŅˆŅ‚ĐžŅŽ", "empty_folder": "ĐĻŅ ĐŋаĐŋĐēа ĐŋĐžŅ€ĐžĐļĐŊŅ", "empty_trash": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐžŅˆĐ¸Đē", - "empty_trash_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐžŅˆĐ¸Đē? ĐĻĐĩ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐ¸Ņ‚ŅŒ Đ˛ŅŅ– Ņ„Đ°ĐšĐģи ҃ ĐēĐžŅˆĐ¸Đē҃ С Immich.\nĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸!", + "empty_trash_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐžŅˆĐ¸Đē? ĐĻĐĩ ĐŊаСавĐļди видаĐģĐ¸Ņ‚ŅŒ С Immich ŅƒŅŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸, Ņ‰Đž ĐŋĐĩŅ€ĐĩĐąŅƒĐ˛Đ°ŅŽŅ‚ŅŒ ҃ ĐēĐžŅˆĐ¸Đē҃.\nĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸!", "enable": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸", "enable_backup": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", - "enable_biometric_auth_description": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ ŅĐ˛Ņ–Đš PIN-ĐēОд, Ņ‰ĐžĐą ŅƒĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐąŅ–ĐžĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐŊ҃ Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–ŅŽ", + "enable_biometric_auth_description": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ PIN-ĐēОд, Ņ‰ĐžĐą ŅƒĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐąŅ–ĐžĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐŊ҃ Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–ŅŽ", "enabled": "ĐŖĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž", "end_date": "Đ”Đ°Ņ‚Đ° СавĐĩŅ€ŅˆĐĩĐŊĐŊŅ", "enqueued": "ĐŖ ҇ĐĩŅ€ĐˇŅ–", "enter_wifi_name": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ ĐŊĐ°ĐˇĐ˛Ņƒ Wi-Fi", - "enter_your_pin_code": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ ŅĐ˛Ņ–Đš PIN-ĐēОд", - "enter_your_pin_code_subtitle": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ ŅĐ˛Ņ–Đš PIN-ĐēОд, Ņ‰ĐžĐą ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Đ´ĐžŅŅ‚ŅƒĐŋ Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи", + "enter_your_pin_code": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ PIN-ĐēОд", + "enter_your_pin_code_subtitle": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ PIN-ĐēОд, Ņ‰ĐžĐą ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Đ´ĐžŅŅ‚ŅƒĐŋ Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи", "error": "ПоĐŧиĐģĐēа", "error_change_sort_album": "НĐĩ вдаĐģĐžŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋĐžŅ€ŅĐ´ĐžĐē ŅĐžŅ€Ņ‚ŅƒĐ˛Đ°ĐŊĐŊŅ аĐģŅŒĐąĐžĐŧ҃", - "error_delete_face": "ПоĐŧиĐģĐēа ĐŋŅ€Đ¸ видаĐģĐĩĐŊĐŊŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ С Ņ„Đ°ĐšĐģ҃", - "error_getting_places": "ПоĐŧиĐģĐēа ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ ĐŧŅ–ŅŅ†ŅŒ", - "error_loading_albums": "ПоĐŧиĐģĐēа СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ аĐģŅŒĐąĐžĐŧŅ–Đ˛", - "error_loading_image": "ПоĐŧиĐģĐēа СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", - "error_loading_partners": "ПоĐŧиĐģĐēа СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Ņ–Đ˛: {error}", - "error_retrieving_asset_information": "ПоĐŧиĐģĐēа ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ— ĐŋŅ€Đž Ņ„Đ°ĐšĐģ", + "error_delete_face": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ ОйĐģĐ¸Ņ‡Ņ‡Ņ С ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°", + "error_getting_places": "НĐĩ вдаĐģĐžŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐŧŅ–ŅŅ†Ņ", + "error_loading_albums": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи", + "error_loading_image": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", + "error_loading_partners": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Ņ–Đ˛: {error}", + "error_retrieving_asset_information": "НĐĩ вдаĐģĐžŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–ŅŽ ĐŋŅ€Đž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚", "error_saving_image": "ПоĐŧиĐģĐēа: {error}", - "error_tag_face_bounding_box": "ПоĐŧиĐģĐēа ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŋОСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡Ņ‡Ņ – ĐŊĐĩ вдаĐģĐžŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐēĐžĐžŅ€Đ´Đ¸ĐŊĐ°Ņ‚Đ¸ Ņ€Đ°ĐŧĐēи", - "error_title": "ПоĐŧиĐģĐēа: Ņ‰ĐžŅŅŒ ĐŋŅ–ŅˆĐģĐž ĐŊĐĩ Ņ‚Đ°Đē", - "error_while_navigating": "ПоĐŧиĐģĐēа ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŋĐĩŅ€ĐĩŅ…ĐžĐ´Ņƒ Đ´Đž Ņ„Đ°ĐšĐģ҃", + "error_tag_face_bounding_box": "НĐĩ вдаĐģĐžŅŅ ĐŋОСĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ОйĐģĐ¸Ņ‡Ņ‡Ņ — ĐŊĐĩ вдаĐģĐžŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐēĐžĐžŅ€Đ´Đ¸ĐŊĐ°Ņ‚Đ¸ Ņ€Đ°ĐŧĐēи", + "error_title": "ПоĐŧиĐģĐēа — Ņ‰ĐžŅŅŒ ĐŋŅ–ŅˆĐģĐž ĐŊĐĩ Ņ‚Đ°Đē", + "error_while_navigating": "НĐĩ вдаĐģĐžŅŅ ĐŋĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°", "errors": { - "cannot_navigate_next_asset": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊĐžĐŗĐž Ņ„Đ°ĐšĐģ҃", - "cannot_navigate_previous_asset": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž Ņ„Đ°ĐšĐģ҃", + "cannot_navigate_next_asset": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊĐžĐŗĐž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°", + "cannot_navigate_previous_asset": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°", "cant_apply_changes": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ СĐŧŅ–ĐŊи", - "cant_change_activity": "НĐĩ ĐŧĐžĐļĐŊа {enabled, select, true {виĐŧĐēĐŊŅƒŅ‚Đ¸} other {ŅƒĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸}} аĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ", - "cant_change_asset_favorite": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐžĐąŅ€Đ°ĐŊĐĩ Đ´ĐģŅ Ņ„Đ°ĐšĐģ҃", - "cant_change_metadata_assets_count": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊŅ– {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", - "cant_get_faces": "НĐĩ ĐŧĐžĐļ҃ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊĐ°Ņ‚Đ¸ ОйĐģĐ¸Ņ‡Ņ‡Ņ", + "cant_change_activity": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ {enabled, select, true {виĐŧĐēĐŊŅƒŅ‚Đ¸} other {ŅƒĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸}} аĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ", + "cant_change_asset_favorite": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐĩ Đ´ĐģŅ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°", + "cant_change_metadata_assets_count": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊŅ– {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "cant_get_faces": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ОйĐģĐ¸Ņ‡Ņ‡Ņ", "cant_get_number_of_comments": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐēŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ĐēĐžĐŧĐĩĐŊŅ‚Đ°Ņ€Ņ–Đ˛", "cant_search_people": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ виĐēĐžĐŊĐ°Ņ‚Đ¸ ĐŋĐžŅˆŅƒĐē ĐģŅŽĐ´ĐĩĐš", "cant_search_places": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ виĐēĐžĐŊĐ°Ņ‚Đ¸ ĐŋĐžŅˆŅƒĐē ĐŧŅ–ŅŅ†ŅŒ", - "error_adding_assets_to_album": "ПоĐŧиĐģĐēа дОдаваĐŊĐŊŅ Ņ„Đ°ĐšĐģŅ–Đ˛ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", - "error_adding_users_to_album": "ПоĐŧиĐģĐēа дОдаваĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", - "error_deleting_shared_user": "ПоĐŧиĐģĐēа ĐŋŅ–Đ´ Ņ‡Đ°Ņ видаĐģĐĩĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐˇŅ– ҁĐŋŅ–ĐģҌĐŊиĐŧ Đ´ĐžŅŅ‚ŅƒĐŋĐžĐŧ", - "error_downloading": "ПоĐŧиĐģĐēа СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ {filename}", - "error_hiding_buy_button": "ПоĐŧиĐģĐēа ĐŋŅ€Đ¸ ҁĐŋŅ€ĐžĐąŅ– ĐŋŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐēĐŊĐžĐŋĐē҃ ĐŋĐžĐē҃ĐŋĐēи", - "error_removing_assets_from_album": "ПоĐŧиĐģĐēа видаĐģĐĩĐŊĐŊŅ Ņ„Đ°ĐšĐģŅ–Đ˛ С аĐģŅŒĐąĐžĐŧ҃, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ ĐēĐžĐŊŅĐžĐģҌ Đ´ĐģŅ ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Đ¸Ņ… Đ˛Ņ–Đ´ĐžĐŧĐžŅŅ‚ĐĩĐš", - "error_selecting_all_assets": "ПоĐŧиĐģĐēа Đ˛Đ¸ĐąĐžŅ€Ņƒ Đ˛ŅŅ–Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", - "exclusion_pattern_already_exists": "ĐĻĐĩĐš ŅˆĐ°ĐąĐģĐžĐŊ виĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ вĐļĐĩ ҖҁĐŊŅƒŅ”.", + "error_adding_assets_to_album": "НĐĩ вдаĐģĐžŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", + "error_adding_users_to_album": "НĐĩ вдаĐģĐžŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", + "error_deleting_shared_user": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ ŅƒŅ‡Đ°ŅĐŊиĐēа ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž Đ´ĐžŅŅ‚ŅƒĐŋ҃", + "error_downloading": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ {filename}", + "error_hiding_buy_button": "НĐĩ вдаĐģĐžŅŅ ĐŋŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐēĐŊĐžĐŋĐē҃ Đē҃ĐŋŅ–Đ˛ĐģŅ–", + "error_removing_assets_from_album": "НĐĩ вдаĐģĐžŅŅ виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ С аĐģŅŒĐąĐžĐŧ҃, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ ĐēĐžĐŊŅĐžĐģҌ Đ´ĐģŅ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Đ¸Ņ… Đ˛Ņ–Đ´ĐžĐŧĐžŅŅ‚ĐĩĐš", + "error_selecting_all_assets": "НĐĩ вдаĐģĐžŅŅ Đ˛Đ¸ĐąŅ€Đ°Ņ‚Đ¸ Đ˛ŅŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", + "exclusion_pattern_already_exists": "ĐĻĐĩĐš ŅˆĐ°ĐąĐģĐžĐŊ виĐŊŅŅ‚Đē҃ вĐļĐĩ ҖҁĐŊŅƒŅ”.", "failed_to_create_album": "НĐĩ вдаĐģĐžŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", "failed_to_create_shared_link": "НĐĩ вдаĐģĐžŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "failed_to_edit_shared_link": "НĐĩ вдаĐģĐžŅŅ Đ˛Ņ–Đ´Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "failed_to_get_people": "НĐĩ вдаĐģĐžŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–ŅŽ ĐŋŅ€Đž ĐģŅŽĐ´ĐĩĐš", - "failed_to_keep_this_delete_others": "НĐĩ вдаĐģĐžŅŅ СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ ҆ĐĩĐš Ņ„Đ°ĐšĐģ Ņ– видаĐģĐ¸Ņ‚Đ¸ Ņ–ĐŊŅˆŅ– Ņ„Đ°ĐšĐģи", - "failed_to_load_asset": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ", - "failed_to_load_assets": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", + "failed_to_keep_this_delete_others": "НĐĩ вдаĐģĐžŅŅ СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ ҆ĐĩĐš ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ Ņ– видаĐģĐ¸Ņ‚Đ¸ Ņ–ĐŊŅˆŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", + "failed_to_load_asset": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚", + "failed_to_load_assets": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", "failed_to_load_notifications": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ", - "failed_to_load_people": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš", - "failed_to_remove_product_key": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡ ĐŋŅ€ĐžĐ´ŅƒĐēŅ‚Ņƒ", + "failed_to_load_people": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ҁĐŋĐ¸ŅĐžĐē ĐģŅŽĐ´ĐĩĐš", + "failed_to_remove_product_key": "НĐĩ вдаĐģĐžŅŅ виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡ ĐŋŅ€ĐžĐ´ŅƒĐēŅ‚Ņƒ", "failed_to_reset_pin_code": "НĐĩ вдаĐģĐžŅŅ ҁĐēиĐŊŅƒŅ‚Đ¸ PIN-ĐēОд", - "failed_to_stack_assets": "НĐĩ вдаĐģĐžŅŅ ĐˇĐŗĐžŅ€ĐŊŅƒŅ‚Đ¸ Ņ„Đ°ĐšĐģи", - "failed_to_unstack_assets": "НĐĩ вдаĐģĐžŅŅ Ņ€ĐžĐˇĐŗĐžŅ€ĐŊŅƒŅ‚Đ¸ Ņ„Đ°ĐšĐģи", + "failed_to_stack_assets": "НĐĩ вдаĐģĐžŅŅ ĐˇĐŗŅ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", + "failed_to_unstack_assets": "НĐĩ вдаĐģĐžŅŅ Ņ€ĐžĐˇĐŗŅ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", "failed_to_update_notification_status": "НĐĩ вдаĐģĐžŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ŅŅ‚Đ°Ņ‚ŅƒŅ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ", "incorrect_email_or_password": "НĐĩĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊа Đ°Đ´Ņ€ĐĩŅĐ° ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅ— ĐŋĐžŅˆŅ‚Đ¸ айО ĐŋĐ°Ņ€ĐžĐģҌ", "library_folder_already_exists": "ĐĻĐĩĐš ҈ĐģŅŅ… Ņ–ĐŧĐŋĐžŅ€Ņ‚Ņƒ вĐļĐĩ ҖҁĐŊŅƒŅ”.", - "page_not_found": "ĐĄŅ‚ĐžŅ€Ņ–ĐŊĐēа ĐŊĐĩ СĐŊаКдĐĩĐŊа", - "paths_validation_failed": "{paths, plural, one {# ҈ĐģŅŅ…} few {# ҈ĐģŅŅ…Đ¸} many {# ҈ĐģŅŅ…Ņ–Đ˛} other {# ҈ĐģŅŅ…Ņƒ}} ĐŊĐĩ ĐŋŅ€ĐžĐšŅˆĐģĐž ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đē҃", - "profile_picture_transparent_pixels": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋŅ€ĐžŅ„Ņ–ĐģŅŽ ĐŊĐĩ ĐŧĐžĐļĐĩ ĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐŋŅ€ĐžĐˇĐžŅ€Đ¸Ņ… ĐŋŅ–ĐēҁĐĩĐģŅ–Đ˛. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐˇĐąŅ–ĐģŅŒŅˆŅ–Ņ‚ŅŒ ĐŧĐ°ŅŅˆŅ‚Đ°Đą Ņ‚Đ°/айО ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Ņ–Ņ‚ŅŒ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ.", - "quota_higher_than_disk_size": "Ви Đ˛ŅŅ‚Đ°ĐŊОвиĐģи ĐēĐ˛ĐžŅ‚Ņƒ, Ņ‰Đž ĐŋĐĩŅ€ĐĩĐ˛Đ¸Ņ‰ŅƒŅ” Ņ€ĐžĐˇĐŧŅ–Ņ€ Đ´Đ¸ŅĐēа", + "page_not_found": "ĐĄŅ‚ĐžŅ€Ņ–ĐŊĐē҃ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž", + "paths_validation_failed": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēа ĐŊĐĩ ĐŋŅ€ĐžĐšĐ´ĐĩĐŊа Đ´ĐģŅ {paths, plural, one {# ҈ĐģŅŅ…Ņƒ} few {# ҈ĐģŅŅ…Ņ–Đ˛} many {# ҈ĐģŅŅ…Ņ–Đ˛} other {# ҈ĐģŅŅ…Ņ–Đ˛}}", + "profile_picture_transparent_pixels": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋŅ€ĐžŅ„Ņ–ĐģŅŽ ĐŊĐĩ ĐŧĐžĐļĐĩ ĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐŋŅ€ĐžĐˇĐžŅ€Đ¸Ņ… ĐŋŅ–ĐēҁĐĩĐģŅ–Đ˛. Đ—ĐąŅ–ĐģŅŒŅˆŅ–Ņ‚ŅŒ ĐŧĐ°ŅŅˆŅ‚Đ°Đą Ņ‚Đ°/айО ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Ņ–Ņ‚ŅŒ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ.", + "quota_higher_than_disk_size": "Ви ŅƒŅŅ‚Đ°ĐŊОвиĐģи ĐēĐ˛ĐžŅ‚Ņƒ, Ņ‰Đž ĐŋĐĩŅ€ĐĩĐ˛Đ¸Ņ‰ŅƒŅ” Ņ€ĐžĐˇĐŧŅ–Ņ€ Đ´Đ¸ŅĐēа", "something_went_wrong": "ĐŠĐžŅŅŒ ĐŋŅ–ŅˆĐģĐž ĐŊĐĩ Ņ‚Đ°Đē", - "unable_to_add_album_users": "НĐĩĐŧĐžĐļĐģивО Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", - "unable_to_add_assets_to_shared_link": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи Đ´Đž ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", - "unable_to_add_comment": "НĐĩĐŧĐžĐļĐģивО Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐēĐžĐŧĐĩĐŊŅ‚Đ°Ņ€", - "unable_to_add_exclusion_pattern": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ виĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ", + "unable_to_add_album_users": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", + "unable_to_add_assets_to_shared_link": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ Đ´Đž ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", + "unable_to_add_comment": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐēĐžĐŧĐĩĐŊŅ‚Đ°Ņ€", + "unable_to_add_exclusion_pattern": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ виĐŊŅŅ‚Đē҃", "unable_to_add_partners": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Ņ–Đ˛", - "unable_to_add_remove_archive": "НĐĩĐŧĐžĐļĐģивО {archived, select, true {виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ Ņ–Đˇ} other {Đ´ĐžĐ´Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģ Đ´Đž}} Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ", - "unable_to_add_remove_favorites": "НĐĩĐŧĐžĐļĐģивО {favorite, select, true {Đ´ĐžĐ´Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģ Đ´Đž} other {виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ Ņ–Đˇ}} ĐžĐąŅ€Đ°ĐŊĐ¸Ņ…", - "unable_to_archive_unarchive": "НĐĩĐŧĐžĐļĐģивО {archived, select, true {Đ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸} other {Ņ€ĐžĐˇĐ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸}}", - "unable_to_change_album_user_role": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Ņ€ĐžĐģҌ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° аĐģŅŒĐąĐžĐŧ҃", - "unable_to_change_date": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ", - "unable_to_change_description": "НĐĩ вдаĐģĐžŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐžĐŋĐ¸Ņ", - "unable_to_change_favorite": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ŅŅ‚Đ°Ņ‚ŅƒŅ ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž Đ´ĐģŅ Ņ„Đ°ĐšĐģ҃", - "unable_to_change_location": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", + "unable_to_add_remove_archive": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ {archived, select, true {виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ Ņ–Đˇ} other {Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ Đ´Đž}} Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ", + "unable_to_add_remove_favorites": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ {favorite, select, true {Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ Đ´Đž} other {виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ Ņ–Đˇ}} Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž", + "unable_to_archive_unarchive": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ {archived, select, true {Đ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸} other {виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ}}", + "unable_to_change_album_user_role": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Ņ€ĐžĐģҌ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° аĐģŅŒĐąĐžĐŧ҃", + "unable_to_change_date": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ", + "unable_to_change_description": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐžĐŋĐ¸Ņ", + "unable_to_change_favorite": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ŅŅ‚Đ°Ņ‚ŅƒŅ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž Đ´ĐģŅ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°", + "unable_to_change_location": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆Đĩ", "unable_to_change_password": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", - "unable_to_change_visibility": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ видиĐŧŅ–ŅŅ‚ŅŒ Đ´ĐģŅ {count, plural, one {# ĐžŅĐžĐąĐ¸} few {# ĐžŅŅ–Đą} other {# ĐģŅŽĐ´ĐĩĐš}}", - "unable_to_complete_oauth_login": "НĐĩĐŧĐžĐļĐģивО СавĐĩŅ€ŅˆĐ¸Ņ‚Đ¸ Đ˛Ņ…Ņ–Đ´ ҇ĐĩŅ€ĐĩС OAuth", - "unable_to_connect": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋŅ–Đ´ĐēĐģŅŽŅ‡Đ¸Ņ‚Đ¸ŅŅ", - "unable_to_copy_to_clipboard": "НĐĩĐŧĐžĐļĐģивО ҁĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧŅ–ĐŊ҃. ПĐĩŅ€ĐĩĐēĐžĐŊĐ°ĐšŅ‚ĐĩŅŅ, Ņ‰Đž ви ĐˇĐ°Ņ…ĐžĐ´Đ¸Ņ‚Đĩ ĐŊа ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃ ҇ĐĩŅ€ĐĩС https", - "unable_to_create": "НĐĩ вдаĐģĐžŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", - "unable_to_create_admin_account": "НĐĩĐŧĐžĐļĐģивО ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ОйĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", - "unable_to_create_api_key": "НĐĩĐŧĐžĐļĐģивО ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊОвиК ĐēĐģŅŽŅ‡ API", - "unable_to_create_library": "НĐĩ вдаĐģĐžŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", - "unable_to_create_user": "НĐĩ вдаĐģĐžŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", + "unable_to_change_visibility": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ видиĐŧŅ–ŅŅ‚ŅŒ Đ´ĐģŅ {count, plural, one {# ĐģŅŽĐ´Đ¸ĐŊи} few {# ĐģŅŽĐ´ĐĩĐš} many {# ĐģŅŽĐ´ĐĩĐš} other {# ĐģŅŽĐ´ĐĩĐš}}", + "unable_to_complete_oauth_login": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СавĐĩŅ€ŅˆĐ¸Ņ‚Đ¸ Đ˛Ņ…Ņ–Đ´ ҇ĐĩŅ€ĐĩС OAuth", + "unable_to_connect": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋŅ–Đ´'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ŅŅ", + "unable_to_copy_to_clipboard": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ҁĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧŅ–ĐŊ҃. ПĐĩŅ€ĐĩĐēĐžĐŊĐ°ĐšŅ‚ĐĩŅŅ, Ņ‰Đž ви ĐˇĐ°Ņ…ĐžĐ´Đ¸Ņ‚Đĩ ĐŊа ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃ ҇ĐĩŅ€ĐĩС https", + "unable_to_create": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–ŅŽ", + "unable_to_create_admin_account": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ОйĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", + "unable_to_create_api_key": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊОвиК ĐēĐģŅŽŅ‡ API", + "unable_to_create_library": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", + "unable_to_create_user": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "unable_to_delete_album": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", - "unable_to_delete_asset": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ", - "unable_to_delete_assets": "ПоĐŧиĐģĐēа видаĐģĐĩĐŊĐŊŅ Ņ„Đ°ĐšĐģŅ–Đ˛", - "unable_to_delete_exclusion_pattern": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ виĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ", - "unable_to_delete_shared_link": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", + "unable_to_delete_asset": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚", + "unable_to_delete_assets": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", + "unable_to_delete_exclusion_pattern": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ виĐŊŅŅ‚Đē҃", + "unable_to_delete_shared_link": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "unable_to_delete_user": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "unable_to_delete_workflow": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", - "unable_to_download_files": "НĐĩĐŧĐžĐļĐģивО СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", - "unable_to_edit_exclusion_pattern": "НĐĩ вдаĐģĐžŅŅ Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ виĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ", - "unable_to_empty_trash": "НĐĩĐŧĐžĐļĐģивО ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐžŅˆĐ¸Đē", - "unable_to_enter_fullscreen": "НĐĩĐŧĐžĐļĐģивО ŅƒĐ˛Ņ–ĐšŅ‚Đ¸ в ĐŋОвĐŊĐžĐĩĐēŅ€Đ°ĐŊĐŊиК Ņ€ĐĩĐļиĐŧ", - "unable_to_exit_fullscreen": "НĐĩĐŧĐžĐļĐģивО Đ˛Đ¸ĐšŅ‚Đ¸ С ĐŋОвĐŊĐžĐĩĐēŅ€Đ°ĐŊĐŊĐžĐŗĐž Ņ€ĐĩĐļиĐŧ҃", - "unable_to_get_comments_number": "НĐĩ вдаĐģĐžŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐēŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ĐēĐžĐŧĐĩĐŊŅ‚Đ°Ņ€Ņ–Đ˛", - "unable_to_get_shared_link": "НĐĩ вдаĐģĐžŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", - "unable_to_hide_person": "НĐĩĐŧĐžĐļĐģивО ĐŋŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐģŅŽĐ´Đ¸ĐŊ҃", - "unable_to_link_motion_video": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Св'ŅĐˇĐ°Ņ‚Đ¸ Ņ€ŅƒŅ…ĐžĐŧĐĩ Đ˛Ņ–Đ´ĐĩĐž", - "unable_to_link_oauth_account": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋŅ€Đ¸Đ˛'ŅĐˇĐ°Ņ‚Đ¸ ОйĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ OAuth", + "unable_to_delete_workflow": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–ŅŽ", + "unable_to_download_files": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", + "unable_to_edit_exclusion_pattern": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ виĐŊŅŅ‚Đē҃", + "unable_to_empty_trash": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐžŅˆĐ¸Đē", + "unable_to_enter_fullscreen": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ŅƒĐ˛Ņ–ĐšŅ‚Đ¸ в ĐŋОвĐŊĐžĐĩĐēŅ€Đ°ĐŊĐŊиК Ņ€ĐĩĐļиĐŧ", + "unable_to_exit_fullscreen": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Đ¸ĐšŅ‚Đ¸ С ĐŋОвĐŊĐžĐĩĐēŅ€Đ°ĐŊĐŊĐžĐŗĐž Ņ€ĐĩĐļиĐŧ҃", + "unable_to_get_comments_number": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐēŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ĐēĐžĐŧĐĩĐŊŅ‚Đ°Ņ€Ņ–Đ˛", + "unable_to_get_shared_link": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", + "unable_to_hide_person": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐģŅŽĐ´Đ¸ĐŊ҃", + "unable_to_link_motion_video": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋŅ€Đ¸Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Ņ€ŅƒŅ…ĐžĐŧĐĩ Đ˛Ņ–Đ´ĐĩĐž", + "unable_to_link_oauth_account": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋŅ€Đ¸Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ ОйĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ OAuth", "unable_to_log_out_all_devices": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Đ¸ĐšŅ‚Đ¸ С ŅƒŅŅ–Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—Đ˛", "unable_to_log_out_device": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Đ¸ĐšŅ‚Đ¸ С ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", "unable_to_login_with_oauth": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ŅƒĐ˛Ņ–ĐšŅ‚Đ¸ Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ OAuth", "unable_to_play_video": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Đ˛Ņ–Đ´ĐĩĐž", - "unable_to_reassign_assets_existing_person": "НĐĩ вдаĐģĐžŅŅ ĐŋĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи {name, select, null {ҖҁĐŊŅƒŅŽŅ‡Ņ–Đš ĐžŅĐžĐąŅ–} other {{name}}}", - "unable_to_reassign_assets_new_person": "НĐĩĐŧĐžĐļĐģивО ĐŋĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ĐŊĐžĐ˛Ņ–Đš ĐžŅĐžĐąŅ–", - "unable_to_refresh_user": "НĐĩ вдаĐģĐžŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "unable_to_remove_album_users": "НĐĩĐŧĐžĐļĐģивО видаĐģĐ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ С аĐģŅŒĐąĐžĐŧ҃", - "unable_to_remove_api_key": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡ API", - "unable_to_remove_assets_from_shared_link": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ĐˇŅ– ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", - "unable_to_remove_library": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", - "unable_to_remove_partner": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°", - "unable_to_remove_reaction": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ Ņ€ĐĩаĐēŅ†Ņ–ŅŽ", + "unable_to_reassign_assets_existing_person": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ {name, select, null {ĐŊĐ°ŅĐ˛ĐŊŅ–Đš ĐģŅŽĐ´Đ¸ĐŊŅ–} other {{name}}}", + "unable_to_reassign_assets_new_person": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŊĐžĐ˛Ņ–Đš ĐģŅŽĐ´Đ¸ĐŊŅ–", + "unable_to_refresh_user": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", + "unable_to_remove_album_users": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ С аĐģŅŒĐąĐžĐŧ҃", + "unable_to_remove_api_key": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡ API", + "unable_to_remove_assets_from_shared_link": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐˇŅ– ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", + "unable_to_remove_library": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", + "unable_to_remove_partner": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°", + "unable_to_remove_reaction": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ Ņ€ĐĩаĐēŅ†Ņ–ŅŽ", "unable_to_reset_password": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ҁĐēиĐŊŅƒŅ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", - "unable_to_reset_pin_code": "НĐĩĐŧĐžĐļĐģивО ҁĐēиĐŊŅƒŅ‚Đ¸ PIN-ĐēОд", - "unable_to_resolve_duplicate": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Đ¸Ņ€Ņ–ŅˆĐ¸Ņ‚Đ¸ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚", - "unable_to_restore_assets": "НĐĩĐŧĐžĐļĐģивО Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", - "unable_to_restore_trash": "НĐĩ вдаĐģĐžŅŅ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ вĐŧҖҁ҂", + "unable_to_reset_pin_code": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ҁĐēиĐŊŅƒŅ‚Đ¸ PIN-ĐēОд", + "unable_to_resolve_duplicate": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŋŅ€Đ°Ņ†ŅŽĐ˛Đ°Ņ‚Đ¸ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚", + "unable_to_restore_assets": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", + "unable_to_restore_trash": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ вĐŧҖҁ҂ ĐēĐžŅˆĐ¸Đēа", "unable_to_restore_user": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "unable_to_save_album": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ аĐģŅŒĐąĐžĐŧ", "unable_to_save_api_key": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ ĐēĐģŅŽŅ‡ API", - "unable_to_save_date_of_birth": "НĐĩ вдаĐģĐžŅŅ СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ Đ´Đ°Ņ‚Ņƒ ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ", + "unable_to_save_date_of_birth": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ Đ´Đ°Ņ‚Ņƒ ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ", "unable_to_save_name": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ Ņ–Đŧ'Ņ", "unable_to_save_profile": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ ĐŋŅ€ĐžŅ„Ņ–ĐģҌ", "unable_to_save_settings": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", "unable_to_scan_libraries": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋŅ€ĐžŅĐēаĐŊŅƒĐ˛Đ°Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", - "unable_to_scan_library": "НĐĩ вдаĐģĐžŅŅ ĐŋŅ€ĐžŅĐēаĐŊŅƒĐ˛Đ°Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", - "unable_to_set_feature_photo": "НĐĩ вдаĐģĐžŅŅ Đ˛ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–ŅŽ ĐŊа ОйĐēĐģадиĐŊĐē҃", - "unable_to_set_profile_picture": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋŅ€ĐžŅ„Ņ–ĐģŅŽ", - "unable_to_set_rating": "НĐĩ вдаĐģĐžŅŅ Đ˛ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", - "unable_to_submit_job": "НĐĩ вдаĐģĐžŅŅ ĐŊĐ°Đ´Ņ–ŅĐģĐ°Ņ‚Đ¸ СавдаĐŊĐŊŅ", - "unable_to_trash_asset": "НĐĩĐŧĐžĐļĐģивО видаĐģĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ", - "unable_to_unlink_account": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Ņ–Đ´Đ˛'ŅĐˇĐ°Ņ‚Đ¸ ОйĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ", + "unable_to_scan_library": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋŅ€ĐžŅĐēаĐŊŅƒĐ˛Đ°Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", + "unable_to_set_feature_photo": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ŅƒŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐŗĐžĐģОвĐŊĐĩ Ņ„ĐžŅ‚Đž", + "unable_to_set_profile_picture": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ŅƒŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋŅ€ĐžŅ„Ņ–ĐģŅŽ", + "unable_to_set_rating": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ŅƒŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", + "unable_to_submit_job": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŊĐ°Đ´Ņ–ŅĐģĐ°Ņ‚Đ¸ СавдаĐŊĐŊŅ", + "unable_to_trash_asset": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ Đ´Đž ĐēĐžŅˆĐ¸Đēа", + "unable_to_unlink_account": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Ņ–Đ´'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ ОйĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ", "unable_to_unlink_motion_video": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Ņ–Đ´'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Ņ€ŅƒŅ…ĐžĐŧĐĩ Đ˛Ņ–Đ´ĐĩĐž", - "unable_to_update_album_cover": "НĐĩĐŧĐžĐļĐģивО ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ОйĐēĐģадиĐŊĐē҃ аĐģŅŒĐąĐžĐŧ҃", - "unable_to_update_album_info": "НĐĩĐŧĐžĐļĐģивО ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–ŅŽ ĐŋŅ€Đž аĐģŅŒĐąĐžĐŧ", - "unable_to_update_library": "НĐĩ вдаĐģĐžŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", - "unable_to_update_location": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", + "unable_to_update_album_cover": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ОйĐēĐģадиĐŊĐē҃ аĐģŅŒĐąĐžĐŧ҃", + "unable_to_update_album_info": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–ŅŽ ĐŋŅ€Đž аĐģŅŒĐąĐžĐŧ", + "unable_to_update_library": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", + "unable_to_update_location": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐŧҖҁ҆Đĩ", "unable_to_update_settings": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", - "unable_to_update_timeline_display_status": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ŅŅ‚Đ°ĐŊ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ҈ĐēаĐģи Ņ‡Đ°ŅŅƒ", - "unable_to_update_user": "НĐĩĐŧĐžĐļĐģивО ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ даĐŊŅ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "unable_to_update_workflow": "НĐĩ вдаĐģĐžŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", - "unable_to_upload_file": "НĐĩ вдаĐģĐžŅŅ виваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ" + "unable_to_update_timeline_display_status": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ŅŅ‚Đ°ĐŊ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ Ņ…Ņ€ĐžĐŊĐžĐģĐžĐŗŅ–Ņ—", + "unable_to_update_user": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ даĐŊŅ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", + "unable_to_update_workflow": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–ŅŽ", + "unable_to_upload_file": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ виваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ" }, "errors_text": "ПоĐŧиĐģĐēи", - "exclusion_pattern": "ШайĐģĐžĐŊ виĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ", + "exclusion_pattern": "ШайĐģĐžĐŊ виĐŊŅŅ‚Đē҃", "exif": "Exif", - "exif_bottom_sheet_description": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐžĐŋĐ¸Ņ...", - "exif_bottom_sheet_description_error": "ПоĐŧиĐģĐēа ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐžĐŋĐ¸ŅŅƒ", - "exif_bottom_sheet_details": "ДĐĩŅ‚Đ°ĐģŅ–", + "exif_bottom_sheet_description": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐžĐŋĐ¸Ņâ€Ļ", + "exif_bottom_sheet_description_error": "НĐĩ вдаĐģĐžŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐžĐŋĐ¸Ņ", + "exif_bottom_sheet_details": "ДЕĐĸАЛІ", "exif_bottom_sheet_location": "МІСĐĻЕ", "exif_bottom_sheet_no_description": "БĐĩС ĐžĐŋĐ¸ŅŅƒ", "exif_bottom_sheet_people": "ЛЮДИ", @@ -1176,39 +1178,39 @@ "exit_slideshow": "Đ’Đ¸ĐšŅ‚Đ¸ ĐˇŅ– ҁĐģаКд-ŅˆĐžŅƒ", "expand": "Đ ĐžĐˇĐŗĐžŅ€ĐŊŅƒŅ‚Đ¸", "expand_all": "Đ ĐžĐˇĐŗĐžŅ€ĐŊŅƒŅ‚Đ¸ Đ˛ŅĐĩ", - "experimental_settings_new_asset_list_subtitle": "В Ņ€ĐžĐˇŅ€ĐžĐąŅ†Ņ–", - "experimental_settings_new_asset_list_title": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐĩĐēҁĐŋĐĩŅ€Đ¸ĐŧĐĩĐŊŅ‚Đ°ĐģҌĐŊ҃ ҁҖ҂Đē҃ Ņ„ĐžŅ‚Đž", - "experimental_settings_subtitle": "На вĐģĐ°ŅĐŊиК Ņ€Đ¸ĐˇĐ¸Đē!", + "experimental_settings_new_asset_list_subtitle": "ĐŖ Ņ€ĐžĐˇŅ€ĐžĐąŅ†Ņ–", + "experimental_settings_new_asset_list_title": "ВĐŧиĐēĐ°Ņ‚Đ¸ ĐĩĐēҁĐŋĐĩŅ€Đ¸ĐŧĐĩĐŊŅ‚Đ°ĐģҌĐŊ҃ ҁҖ҂Đē҃ Ņ„ĐžŅ‚Đž", + "experimental_settings_subtitle": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ ĐŊа вĐģĐ°ŅĐŊиК Ņ€Đ¸ĐˇĐ¸Đē!", "experimental_settings_title": "ЕĐēҁĐŋĐĩŅ€Đ¸ĐŧĐĩĐŊŅ‚Đ°ĐģҌĐŊŅ–", - "expire_after": "ĐĸĐĩŅ€ĐŧŅ–ĐŊ Đ´Ņ–Ņ— СаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС", + "expire_after": "ĐĄĐŋĐģĐ¸Đ˛Đ°Ņ” ҇ĐĩŅ€ĐĩС", "expired": "ЗаĐēŅ–ĐŊŅ‡Đ¸Đ˛ŅŅ Ņ‚ĐĩŅ€ĐŧŅ–ĐŊ Đ´Ņ–Ņ—", "expires_date": "ĐĸĐĩŅ€ĐŧŅ–ĐŊ Đ´Ņ–Ņ— СаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ {date}", - "explore": "Đ”ĐžŅĐģŅ–Đ´Đ¸Ņ‚Đ¸", + "explore": "ĐžĐŗĐģŅĐ´", "explorer": "ĐŸŅ€ĐžĐ˛Ņ–Đ´ĐŊиĐē", "export": "ЕĐēҁĐŋĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸", - "export_as_json": "ЕĐēҁĐŋĐžŅ€Ņ‚ в JSON", + "export_as_json": "ЕĐēҁĐŋĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ ŅĐē JSON", "export_database": "ЕĐēҁĐŋĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ ĐąĐ°ĐˇŅƒ даĐŊĐ¸Ņ…", "export_database_description": "ЕĐēҁĐŋĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ ĐąĐ°ĐˇŅƒ даĐŊĐ¸Ņ… SQLite", "extension": "Đ ĐžĐˇŅˆĐ¸Ņ€ĐĩĐŊĐŊŅ", "external": "ЗовĐŊŅ–ŅˆĐŊŅ–", "external_libraries": "ЗовĐŊŅ–ŅˆĐŊŅ– ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", "external_network": "ЗовĐŊŅ–ŅˆĐŊŅ ĐŧĐĩŅ€ĐĩĐļа", - "external_network_sheet_info": "КоĐģи ви ĐŊĐĩ ĐŋŅ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊŅ– Đ´Đž ĐžĐąŅ€Đ°ĐŊĐžŅ— ĐŧĐĩŅ€ĐĩĐļŅ– Wi-Fi, ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē ĐŋŅ–Đ´ĐēĐģŅŽŅ‡Đ°Ņ‚Đ¸ĐŧĐĩŅ‚ŅŒŅŅ Đ´Đž ҁĐĩŅ€Đ˛ĐĩŅ€Đ° ҇ĐĩŅ€ĐĩС ĐŋĐĩŅ€ŅˆŅƒ С ĐŊавĐĩĐ´ĐĩĐŊĐ¸Ņ… ĐŊиĐļ҇Đĩ URL-Đ°Đ´Ņ€Đĩҁ, ŅĐē҃ Đ˛Ņ–ĐŊ СĐŧĐžĐļĐĩ Đ´ĐžŅŅĐŗŅ‚Đ¸, ĐŋĐžŅ‡Đ¸ĐŊĐ°ŅŽŅ‡Đ¸ СвĐĩŅ€Ņ…Ņƒ вĐŊиС", + "external_network_sheet_info": "КоĐģи ви ĐŊĐĩ ĐŋŅ–Đ´'Ņ”Đ´ĐŊаĐŊŅ– Đ´Đž ĐžĐąŅ€Đ°ĐŊĐžŅ— ĐŧĐĩŅ€ĐĩĐļŅ– Wi-Fi, ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē ĐŋŅ–Đ´'Ņ”Đ´ĐŊŅƒĐ˛Đ°Ņ‚Đ¸ĐŧĐĩŅ‚ŅŒŅŅ Đ´Đž ҁĐĩŅ€Đ˛ĐĩŅ€Đ° ҇ĐĩŅ€ĐĩС ĐŋĐĩŅ€ŅˆŅƒ Đ´ĐžŅŅ‚ŅƒĐŋĐŊ҃ URL-Đ°Đ´Ņ€Đĩҁ҃ С ĐŊавĐĩĐ´ĐĩĐŊĐ¸Ņ… ĐŊиĐļ҇Đĩ, ĐŋĐžŅ‡Đ¸ĐŊĐ°ŅŽŅ‡Đ¸ СвĐĩŅ€Ņ…Ņƒ вĐŊиС", "face_unassigned": "НĐĩ ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐž", "failed": "НĐĩ вдаĐģĐžŅŅ", "failed_count": "НĐĩ вдаĐģĐžŅŅ: {count}", - "failed_to_authenticate": "ПоĐŧиĐģĐēа Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ—", - "failed_to_load_assets": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", + "failed_to_authenticate": "НĐĩ вдаĐģĐžŅŅ Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ", + "failed_to_load_assets": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", "failed_to_load_folder": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐŋаĐŋĐē҃", - "favorite": "До ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž", - "favorite_action_prompt": "{count} дОдаĐŊĐž Đ´Đž ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž", - "favorite_or_unfavorite_photo": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž ĐžĐąŅ€Đ°ĐŊĐ¸Ņ… айО видаĐģĐ¸Ņ‚Đ¸ С ĐžĐąŅ€Đ°ĐŊĐ¸Ņ… Ņ„ĐžŅ‚Đž", - "favorites": "ĐžĐąŅ€Đ°ĐŊĐĩ", - "favorites_page_no_favorites": "НĐĩĐŧĐ°Ņ” ĐžĐąŅ€Đ°ĐŊĐ¸Ņ… Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", - "feature_photo_updated": "Đ’Đ¸ĐąŅ€Đ°ĐŊĐĩ Ņ„ĐžŅ‚Đž ĐžĐŊОвĐģĐĩĐŊĐž", - "features": "Đ”ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Ņ– ĐŧĐžĐļĐģĐ¸Đ˛ĐžŅŅ‚Ņ–", + "favorite": "До Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž", + "favorite_action_prompt": "{count} дОдаĐŊĐž Đ´Đž Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž", + "favorite_or_unfavorite_photo": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž Đ´Đž Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž айО виĐģŅƒŅ‡Đ¸Ņ‚Đ¸", + "favorites": "Đ’Đ¸ĐąŅ€Đ°ĐŊĐĩ", + "favorites_page_no_favorites": "НĐĩĐŧĐ°Ņ” Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", + "feature_photo_updated": "ГоĐģОвĐŊĐĩ Ņ„ĐžŅ‚Đž ĐžĐŊОвĐģĐĩĐŊĐž", + "features": "Đ¤ŅƒĐŊĐē҆Җҗ", "features_in_development": "Đ¤ŅƒĐŊĐē҆Җҗ в Ņ€ĐžĐˇŅ€ĐžĐąŅ†Ņ–", - "features_setting_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ Đ´ĐžĐ´Đ°Ņ‚ĐēОвиĐŧи ĐŧĐžĐļĐģĐ¸Đ˛ĐžŅŅ‚ŅĐŧи ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃", + "features_setting_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ„ŅƒĐŊĐēŅ†Ņ–ŅĐŧи ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃", "file_name_or_extension": "ІĐŧ'Ņ Ņ„Đ°ĐšĐģ҃ айО Ņ€ĐžĐˇŅˆĐ¸Ņ€ĐĩĐŊĐŊŅ", "file_name_text": "ІĐŧ'Ņ Ņ„Đ°ĐšĐģ҃", "file_name_with_value": "ІĐŧ'Ņ Ņ„Đ°ĐšĐģ҃: {file_name}", @@ -1216,10 +1218,10 @@ "filename": "ІĐŧ'Ņ Ņ„Đ°ĐšĐģ҃", "filetype": "ĐĸиĐŋ Ņ„Đ°ĐšĐģ҃", "filter": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€", - "filter_description": "ĐŖĐŧОви Đ´ĐģŅ ҄ҖĐģŅŒŅ‚Ņ€Đ°Ņ†Ņ–Ņ— ҆ҖĐģŅŒĐžĐ˛Đ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", + "filter_description": "ĐŖĐŧОви Đ´ĐģŅ ҄ҖĐģŅŒŅ‚Ņ€Đ°Ņ†Ņ–Ņ— ҆ҖĐģŅŒĐžĐ˛Đ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", "filter_people": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€ Са ĐģŅŽĐ´ŅŒĐŧи", "filter_places": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€ Са ĐŧŅ–ŅŅ†ŅĐŧи", - "filter_tags": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ‚ĐĩĐŗĐ¸", + "filter_tags": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€ Са Ņ‚ĐĩĐŗĐ°Đŧи", "filters": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€Đ¸", "find_them_fast": "ШвидĐēĐž СĐŊĐ°Ņ…ĐžĐ´ŅŒŅ‚Đĩ Ņ—Ņ… Са ĐŊĐ°ĐˇĐ˛ĐžŅŽ Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ ĐŋĐžŅˆŅƒĐē҃", "first": "ПĐĩŅ€ŅˆĐ¸Đš", @@ -1227,184 +1229,186 @@ "folder": "ПаĐŋĐēа", "folder_not_found": "ПаĐŋĐē҃ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž", "folders": "ПаĐŋĐēи", - "folders_feature_description": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´ ĐŋаĐŋĐžĐē С Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–ŅĐŧи Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ҃ Ņ„Đ°ĐšĐģĐžĐ˛Ņ–Đš ŅĐ¸ŅŅ‚ĐĩĐŧŅ–", - "forgot_pin_code_question": "Đ—Đ°ĐąŅƒĐģи ŅĐ˛Ņ–Đš PIN-ĐēОд?", + "folders_feature_description": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´ ĐŋаĐŋĐžĐē С Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ҃ Ņ„Đ°ĐšĐģĐžĐ˛Ņ–Đš ŅĐ¸ŅŅ‚ĐĩĐŧŅ–", + "forgot_pin_code_question": "Đ—Đ°ĐąŅƒĐģи PIN-ĐēОд?", "forward": "ПĐĩŅ€ĐĩҁĐģĐ°Ņ‚Đ¸", "free_up_space": "Đ—Đ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆Đĩ", - "free_up_space_description": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Ņ–Ņ‚ŅŒ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš Ņ– Đ˛Ņ–Đ´ĐĩĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ, Ņ‰ĐžĐą ĐˇĐ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆Đĩ. Đ’Đ°ŅˆŅ– ĐēĐžĐŋŅ–Ņ— ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ СаĐģĐ¸ŅˆĐ°ŅŽŅ‚ŅŒŅŅ в ĐąĐĩСĐŋĐĩ҆Җ.", - "free_up_space_settings_subtitle": "Đ—Đ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŋаĐŧ'ŅŅ‚ŅŒ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", + "free_up_space_description": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Ņ–Ņ‚ŅŒ Ņ„ĐžŅ‚Đž Đš Đ˛Ņ–Đ´ĐĩĐž, Đ´ĐģŅ ŅĐēĐ¸Ņ… Ņ” Ņ€ĐĩСĐĩŅ€Đ˛ĐŊа ĐēĐžĐŋŅ–Ņ, Đ´Đž ĐēĐžŅˆĐ¸Đēа Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ, Ņ‰ĐžĐą ĐˇĐ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆Đĩ. Đ’Đ°ŅˆŅ– ĐēĐžĐŋŅ–Ņ— ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ СаĐģĐ¸ŅˆĐ°ŅŽŅ‚ŅŒŅŅ в ĐąĐĩСĐŋĐĩ҆Җ.", + "free_up_space_settings_subtitle": "Đ—Đ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đĩ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", "full_path": "ПовĐŊиК ҈ĐģŅŅ…: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "ĐĻŅ Ņ„ŅƒĐŊĐēŅ†Ņ–Ņ СаваĐŊŅ‚Đ°ĐļŅƒŅ” СОвĐŊŅ–ŅˆĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸ С Google Đ´ĐģŅ ŅĐ˛ĐžŅ”Ņ— Ņ€ĐžĐąĐžŅ‚Đ¸.", "general": "Đ—Đ°ĐŗĐ°ĐģҌĐŊŅ–", - "geolocation_instruction_location": "ĐĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ ĐŊа Ņ„Đ°ĐšĐģ Ņ–Đˇ ĐŗĐĩОдаĐŊиĐŧи, Ņ‰ĐžĐą виĐēĐžŅ€Đ¸ŅŅ‚Đ°Ņ‚Đ¸ ĐšĐžĐŗĐž ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ, айО вийĐĩŅ€Ņ–Ņ‚ŅŒ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ ĐąĐĩСĐŋĐžŅĐĩŅ€ĐĩĐ´ĐŊŅŒĐž ĐŊа ĐŧаĐŋŅ–", + "geolocation_instruction_location": "ĐĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ ĐŊа ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ Ņ–Đˇ ĐŗĐĩОдаĐŊиĐŧи, Ņ‰ĐžĐą виĐēĐžŅ€Đ¸ŅŅ‚Đ°Ņ‚Đ¸ ĐšĐžĐŗĐž ĐŧҖҁ҆Đĩ, айО вийĐĩŅ€Ņ–Ņ‚ŅŒ ĐŧҖҁ҆Đĩ ĐąĐĩСĐŋĐžŅĐĩŅ€ĐĩĐ´ĐŊŅŒĐž ĐŊа ĐŧаĐŋŅ–", "get_help": "ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Đ´ĐžĐŋĐžĐŧĐžĐŗŅƒ", - "get_people_error": "ПоĐŧиĐģĐēа ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ ĐģŅŽĐ´ĐĩĐš", - "get_wifiname_error": "НĐĩ вдаĐģĐžŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐŊĐ°ĐˇĐ˛Ņƒ Wi-Fi. ПĐĩŅ€ĐĩĐēĐžĐŊĐ°ĐšŅ‚ĐĩŅŅ, Ņ‰Đž ви ĐŊадаĐģи ĐŊĐĩĐžĐąŅ…Ņ–Đ´ĐŊŅ– дОСвОĐģи Ņ‚Đ° ĐŋŅ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊŅ– Đ´Đž Wi-Fi ĐŧĐĩŅ€ĐĩĐļŅ–", - "getting_started": "ĐŸĐžŅ‡Đ°Ņ‚ĐžĐē", + "get_people_error": "НĐĩ вдаĐģĐžŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ҁĐŋĐ¸ŅĐžĐē ĐģŅŽĐ´ĐĩĐš", + "get_wifiname_error": "НĐĩ вдаĐģĐžŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐŊĐ°ĐˇĐ˛Ņƒ Wi-Fi. ПĐĩŅ€ĐĩĐēĐžĐŊĐ°ĐšŅ‚ĐĩŅŅ, Ņ‰Đž ви ĐŊадаĐģи ĐŊĐĩĐžĐąŅ…Ņ–Đ´ĐŊŅ– дОСвОĐģи Ņ‚Đ° ĐŋŅ–Đ´'Ņ”Đ´ĐŊаĐŊŅ– Đ´Đž Wi-Fi ĐŧĐĩŅ€ĐĩĐļŅ–", + "getting_started": "ĐŸĐžŅ‡Đ°Ņ‚ĐžĐē Ņ€ĐžĐąĐžŅ‚Đ¸", "go_back": "ПовĐĩŅ€ĐŊŅƒŅ‚Đ¸ŅŅ ĐŊаСад", "go_to_folder": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŋаĐŋĐēи", "go_to_search": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŋĐžŅˆŅƒĐē҃", - "gps": "ГĐĩĐžĐģĐžĐēĐ°Ņ†Ņ–Ņ", + "gps": "GPS", "gps_missing": "НĐĩĐŧĐ°Ņ” ĐŗĐĩОдаĐŊĐ¸Ņ…", "grant_permission": "ĐĐ°Đ´Đ°Ņ‚Đ¸ Đ´ĐžĐˇĐ˛Ņ–Đģ", - "group_albums_by": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи Са...", - "group_country": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ Са ĐēŅ€Đ°Ņ—ĐŊĐžŅŽ", + "group_albums_by": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи Саâ€Ļ", + "group_country": "За ĐēŅ€Đ°Ņ—ĐŊĐžŅŽ", "group_no": "БĐĩС ĐŗŅ€ŅƒĐŋŅƒĐ˛Đ°ĐŊĐŊŅ", "group_owner": "За вĐģĐ°ŅĐŊиĐēĐžĐŧ", - "group_places_by": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧŅ–ŅŅ†Ņ Са...", + "group_places_by": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧŅ–ŅŅ†Ņ Саâ€Ļ", "group_year": "За Ņ€ĐžĐēĐžĐŧ", - "haptic_feedback_switch": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ‚Đ°ĐēŅ‚Đ¸ĐģҌĐŊ҃ Đ˛Ņ–Đ´Đ´Đ°Ņ‡Ņƒ", - "haptic_feedback_title": "ĐĸаĐēŅ‚Đ¸ĐģҌĐŊа Đ˛Ņ–Đ´Đ´Đ°Ņ‡Đ°", - "has_quota": "ĐšĐ˛ĐžŅ‚Đ°", - "hash_asset": "ĐĨĐĩŅˆŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģ", - "hashed_assets": "ĐĨĐĩŅˆĐ¸", + "haptic_feedback_switch": "ВĐŧиĐēĐ°Ņ‚Đ¸ Ņ‚Đ°ĐēŅ‚Đ¸ĐģҌĐŊиК Đ˛Ņ–Đ´ĐŗŅƒĐē", + "haptic_feedback_title": "ĐĸаĐēŅ‚Đ¸ĐģҌĐŊиК Đ˛Ņ–Đ´ĐŗŅƒĐē", + "has_quota": "ĐœĐ°Ņ” ĐēĐ˛ĐžŅ‚Ņƒ", + "hash_asset": "ĐĨĐĩŅˆŅƒĐ˛Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚", + "hashed_assets": "Đ—Đ°Ņ…ĐĩŅˆĐžĐ˛Đ°ĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", "hashing": "ĐĨĐĩŅˆŅƒĐ˛Đ°ĐŊĐŊŅ", "header_settings_add_header_tip": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐˇĐ°ĐŗĐžĐģОвОĐē", "header_settings_field_validator_msg": "ЗĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŊĐĩ ĐŧĐžĐļĐĩ ĐąŅƒŅ‚Đ¸ ĐŋĐžŅ€ĐžĐļĐŊŅ–Đŧ", - "header_settings_header_name_input": "ІĐŧ'Ņ ĐˇĐ°ĐŗĐžĐģОвĐē҃", - "header_settings_header_value_input": "ЗĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐˇĐ°ĐŗĐžĐģОвĐē҃", - "headers_settings_tile_title": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°ĐģҌĐŊĐ¸Ņ†ŅŒĐēŅ– ĐˇĐ°ĐŗĐžĐģОвĐēи ĐŋŅ€ĐžĐēҁҖ", + "header_settings_header_name_input": "Назва ĐˇĐ°ĐŗĐžĐģОвĐēа", + "header_settings_header_value_input": "ЗĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐˇĐ°ĐŗĐžĐģОвĐēа", + "headers_settings_tile_title": "Đ”ĐžĐ˛Ņ–ĐģҌĐŊŅ– ĐˇĐ°ĐŗĐžĐģОвĐēи ĐŋŅ€ĐžĐēҁҖ", "height": "Đ’Đ¸ŅĐžŅ‚Đ°", "hi_user": "ĐŸŅ€Đ¸Đ˛Ņ–Ņ‚ {name} ({email})", - "hide_all_people": "ĐĄŅ…ĐžĐ˛Đ°Ņ‚Đ¸ Đ˛ŅŅ–Ņ…", + "hide_all_people": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ Đ˛ŅŅ–Ņ… ĐģŅŽĐ´ĐĩĐš", "hide_gallery": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐŗĐ°ĐģĐĩŅ€ĐĩŅŽ", - "hide_named_person": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ {name}", + "hide_named_person": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐģŅŽĐ´Đ¸ĐŊ҃ {name}", "hide_password": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", "hide_person": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐģŅŽĐ´Đ¸ĐŊ҃", "hide_schema": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ҁ҅ĐĩĐŧ҃", "hide_text_recognition": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ Ņ‚ĐĩĐēŅŅ‚Ņƒ", - "hide_unnamed_people": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš ĐąĐĩС Ņ–Đŧ'Ņ", - "home_page_add_to_album_conflicts": "ДодаĐŊĐž {added} Ņ„Đ°ĐšĐģŅ–Đ˛ Đ´Đž аĐģŅŒĐąĐžĐŧ҃ {album}. {failed} Ņ„Đ°ĐšĐģŅ–Đ˛ вĐļĐĩ ĐąŅƒĐģĐž в аĐģŅŒĐąĐžĐŧŅ–.", - "home_page_add_to_album_err_local": "НĐĩĐŧĐžĐļĐģивО Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģи Đ´Đž аĐģŅŒĐąĐžĐŧŅ–Đ˛, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", - "home_page_add_to_album_success": "ДодаĐŊĐž {added} Ņ„Đ°ĐšĐģŅ–Đ˛ Đ´Đž аĐģŅŒĐąĐžĐŧ҃ {album}.", - "home_page_album_err_partner": "ПоĐēи Ņ‰Đž ĐŊĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ° Đ´Đž аĐģŅŒĐąĐžĐŧ҃, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", - "home_page_archive_err_local": "ПоĐēи Ņ‰Đž ĐŊĐĩĐŧĐžĐļĐģивО ĐˇĐ°Đ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģи, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", - "home_page_archive_err_partner": "НĐĩĐŧĐžĐļĐģивО Đ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", + "hide_unnamed_people": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš ĐąĐĩС Ņ–ĐŧĐĩĐŊŅ–", + "home_page_add_to_album_conflicts": "ДодаĐŊĐž {added} ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ Đ´Đž аĐģŅŒĐąĐžĐŧ҃ {album}. {failed} ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ вĐļĐĩ Ņ” в аĐģŅŒĐąĐžĐŧŅ–.", + "home_page_add_to_album_err_local": "ĐĐ°Ņ€Đ°ĐˇŅ– ĐŊĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧŅ–Đ˛, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "home_page_add_to_album_success": "ДодаĐŊĐž {added} ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ Đ´Đž аĐģŅŒĐąĐžĐŧ҃ {album}.", + "home_page_album_err_partner": "ĐĐ°Ņ€Đ°ĐˇŅ– ĐŊĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ° Đ´Đž аĐģŅŒĐąĐžĐŧ҃, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "home_page_archive_err_local": "ĐĐ°Ņ€Đ°ĐˇŅ– ĐŊĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "home_page_archive_err_partner": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", "home_page_building_timeline": "ĐŸĐžĐąŅƒĐ´ĐžĐ˛Đ° Ņ…Ņ€ĐžĐŊĐžĐģĐžĐŗŅ–Ņ—", - "home_page_delete_err_partner": "НĐĩĐŧĐžĐļĐģивО видаĐģĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", - "home_page_delete_remote_err_local": "ЛоĐēаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģ(и) вĐļĐĩ в ĐŋŅ€ĐžŅ†ĐĩҁҖ видаĐģĐĩĐŊĐŊŅ С ҁĐĩŅ€Đ˛ĐĩŅ€Đ°, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", - "home_page_favorite_err_local": "ПоĐēи Ņ‰Đž ĐŊĐĩ ĐŧĐžĐļĐŊа Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž ĐģĐžĐēаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģи, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", - "home_page_favorite_err_partner": "ПоĐēи Ņ‰Đž ĐŊĐĩ ĐŧĐžĐļĐŊа Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž Ņ„Đ°ĐšĐģи ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", - "home_page_first_time_notice": "Đ¯ĐēŅ‰Đž ви ĐēĐžŅ€Đ¸ŅŅ‚ŅƒŅ”Ņ‚ĐĩŅŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐēĐžĐŧ вĐŋĐĩŅ€ŅˆĐĩ, ĐąŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ОйĐĩŅ€Ņ–Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧ Đ´ĐģŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ, Ņ‰ĐžĐą ĐŊа ҈ĐēаĐģŅ– Ņ‡Đ°ŅŅƒ Đˇâ€™ŅĐ˛Đ¸ĐģĐ¸ŅŅ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", - "home_page_locked_error_local": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģи Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", - "home_page_locked_error_partner": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€ŅŅŒĐēŅ– Ņ„Đ°ĐšĐģи Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", - "home_page_share_err_local": "НĐĩĐŧĐžĐļĐģивО ĐŋĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ ĐģĐžĐēаĐģҌĐŊиĐŧи Ņ„Đ°ĐšĐģаĐŧи ҇ĐĩŅ€ĐĩС ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", - "home_page_upload_err_limit": "МоĐļĐŊа виваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐĩ ĐąŅ–ĐģҌ҈Đĩ 30 Ņ„Đ°ĐšĐģŅ–Đ˛ вОдĐŊĐžŅ‡Đ°Ņ, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", + "home_page_delete_err_partner": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "home_page_delete_remote_err_local": "ĐĄĐĩŅ€ĐĩĐ´ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… Đ´ĐģŅ видаĐģĐĩĐŊĐŊŅ С ҁĐĩŅ€Đ˛ĐĩŅ€Đ° Ņ” ĐģĐžĐēаĐģҌĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "home_page_favorite_err_local": "ĐĐ°Ņ€Đ°ĐˇŅ– ĐŊĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ Đ´Đž Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "home_page_favorite_err_partner": "ĐĐ°Ņ€Đ°ĐˇŅ– ĐŊĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ° Đ´Đž Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "home_page_first_time_notice": "Đ¯ĐēŅ‰Đž ви ĐēĐžŅ€Đ¸ŅŅ‚ŅƒŅ”Ņ‚ĐĩŅŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐēĐžĐŧ ҃ĐŋĐĩŅ€ŅˆĐĩ, ĐąŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ОйĐĩŅ€Ņ–Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧ Đ´ĐģŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ, Ņ‰ĐžĐą ҃ Ņ…Ņ€ĐžĐŊĐžĐģĐžĐŗŅ–Ņ— Đˇâ€™ŅĐ˛Đ¸ĐģĐ¸ŅŅ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", + "home_page_locked_error_local": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "home_page_locked_error_partner": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ° Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "home_page_share_err_local": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŊĐ°Đ´Đ°Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ Đ´Đž ĐģĐžĐēаĐģҌĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ҇ĐĩŅ€ĐĩС ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "home_page_upload_err_limit": "МоĐļĐŊа виваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐĩ ĐąŅ–ĐģҌ҈Đĩ 30 ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ вОдĐŊĐžŅ‡Đ°Ņ, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", "host": "ĐĨĐžŅŅ‚", "hour": "ГодиĐŊа", "hours": "ГодиĐŊи", "id": "ID", "idle": "ĐŸŅ€ĐžŅŅ‚Ņ–Đš", - "ignore_icloud_photos": "ĐŸŅ€ĐžĐŋ҃ҁĐēĐ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи С iCloud", - "ignore_icloud_photos_description": "НĐĩ СаваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи в Immich, ŅĐēŅ‰Đž вОĐŊи СйĐĩŅ€Ņ–ĐŗĐ°ŅŽŅ‚ŅŒŅŅ в iCloud", + "ignore_icloud_photos": "ĐŸŅ€ĐžĐŋ҃ҁĐēĐ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž С iCloud", + "ignore_icloud_photos_description": "Đ¤ĐžŅ‚Đž, Ņ‰Đž СйĐĩŅ€Ņ–ĐŗĐ°ŅŽŅ‚ŅŒŅŅ в iCloud, ĐŊĐĩ ĐąŅƒĐ´Đĩ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐž ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€ Immich", "image": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "image_alt_text_date": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} СĐŊŅŅ‚Đž {date}", - "image_alt_text_date_1_person": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} С {person1} ĐˇŅ€ĐžĐąĐģĐĩĐŊĐž {date}", - "image_alt_text_date_2_people": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} С {person1} Ņ‚Đ° {person2} ĐˇŅ€ĐžĐąĐģĐĩĐŊĐž {date}", - "image_alt_text_date_3_people": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} С {person1}, {person2} Ņ– {person3} ĐˇŅ€ĐžĐąĐģĐĩĐŊĐž {date}", - "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} С {person1}, {person2} Ņ‚Đ° ҉Đĩ {additionalCount, number} ĐžŅĐžĐąĐ°Đŧи ĐˇŅ€ĐžĐąĐģĐĩĐŊĐž {date}", - "image_alt_text_date_place": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} ĐˇŅ€ĐžĐąĐģĐĩĐŊĐž в {city}, {country} {date}", - "image_alt_text_date_place_1_person": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} ĐˇŅ€ĐžĐąĐģĐĩĐŊĐž в {city}, {country} С {person1} {date}", - "image_alt_text_date_place_2_people": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} ĐˇŅ€ĐžĐąĐģĐĩĐŊĐž в {city}, {country} С {person1} Ņ‚Đ° {person2} {date}", - "image_alt_text_date_place_3_people": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} ĐˇŅ€ĐžĐąĐģĐĩĐŊĐž в {city}, {country} С {person1}, {person2} Ņ‚Đ° {person3} {date}", - "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} ĐˇŅ€ĐžĐąĐģĐĩĐŊĐž в {city}, {country} С {person1}, {person2} Ņ‚Đ° ҉Đĩ {additionalCount, number} ĐžŅĐžĐąĐ°Đŧи {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} С {person1} СĐŊŅŅ‚Đž {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} С {person1} Ņ‚Đ° {person2} СĐŊŅŅ‚Đž {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} С {person1}, {person2} Ņ– {person3} СĐŊŅŅ‚Đž {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} С {person1}, {person2} Ņ‚Đ° ҉Đĩ {additionalCount, number} ĐģŅŽĐ´ŅŒĐŧи СĐŊŅŅ‚Đž {date}", + "image_alt_text_date_place": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} СĐŊŅŅ‚Đž в {city}, {country} {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} СĐŊŅŅ‚Đž в {city}, {country} С {person1} {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} СĐŊŅŅ‚Đž в {city}, {country} С {person1} Ņ‚Đ° {person2} {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} СĐŊŅŅ‚Đž в {city}, {country} С {person1}, {person2} Ņ‚Đ° {person3} {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} СĐŊŅŅ‚Đž в {city}, {country} С {person1}, {person2} Ņ‚Đ° ҉Đĩ {additionalCount, number} ĐģŅŽĐ´ŅŒĐŧи {date}", "image_saved_successfully": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ СйĐĩŅ€ĐĩĐļĐĩĐŊĐž", "image_viewer_page_state_provider_download_started": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŋĐžŅ‡Đ°ĐģĐžŅŅ", - "image_viewer_page_state_provider_download_success": "ĐŖŅĐŋŅ–ŅˆĐŊĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž", - "image_viewer_page_state_provider_share_error": "ПоĐŧиĐģĐēа ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž Đ´ĐžŅŅ‚ŅƒĐŋ҃", + "image_viewer_page_state_provider_download_success": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž", + "image_viewer_page_state_provider_share_error": "НĐĩ вдаĐģĐžŅŅ ĐŊĐ°Đ´Đ°Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ", "immich_logo": "Đ›ĐžĐŗĐžŅ‚Đ¸Đŋ Immich", "immich_web_interface": "ВĐĩĐą-Ņ–ĐŊŅ‚ĐĩҀ҄ĐĩĐšŅ Immich", - "import_from_json": "ІĐŧĐŋĐžŅ€Ņ‚ С JSON", + "import_from_json": "ІĐŧĐŋĐžŅ€Ņ‚ Ņ–Đˇ JSON", "import_path": "ШĐģŅŅ… Ņ–ĐŧĐŋĐžŅ€Ņ‚Ņƒ", "in_albums": "ĐŖ {count, plural, one {# аĐģŅŒĐąĐžĐŧŅ–} few {# аĐģŅŒĐąĐžĐŧĐ°Ņ…} many {# аĐģŅŒĐąĐžĐŧĐ°Ņ…} other {# аĐģŅŒĐąĐžĐŧĐ°Ņ…}}", "in_archive": "В Đ°Ņ€Ņ…Ņ–Đ˛Ņ–", "in_year": "ĐŖ {year}", "in_year_selector": "ĐŖ", - "include_archived": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ‚Đ¸ Đ°Ņ€Ņ…Ņ–Đ˛", - "include_shared_albums": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊŅ– аĐģŅŒĐąĐžĐŧи", - "include_shared_partner_assets": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°", - "individual_share": "ІĐŊĐ´Đ¸Đ˛Ņ–Đ´ŅƒĐ°ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ", - "individual_shares": "ОĐēŅ€ĐĩĐŧŅ– ҁĐŋŅ–ĐģҌĐŊŅ– Đ´ĐžŅŅ‚ŅƒĐŋи", + "include_archived": "Đ’Ņ€Đ°Ņ…ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Đ°Ņ€Ņ…Ņ–Đ˛ĐžĐ˛Đ°ĐŊŅ–", + "include_shared_albums": "Đ’Ņ€Đ°Ņ…ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊŅ– аĐģŅŒĐąĐžĐŧи", + "include_shared_partner_assets": "Đ’Ņ€Đ°Ņ…ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°", + "individual_share": "ОĐēŅ€ĐĩĐŧиК Đ´ĐžŅŅ‚ŅƒĐŋ", + "individual_shares": "ОĐēŅ€ĐĩĐŧŅ– Đ´ĐžŅŅ‚ŅƒĐŋи", "info": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ", "interval": { "day_at_onepm": "ЊОдĐŊŅ Đž 13:00", - "hours": "КоĐļĐŊ҃ {hours, plural, one {ĐŗĐžĐ´Đ¸ĐŊ҃} few {ĐŗĐžĐ´Đ¸ĐŊи} many {ĐŗĐžĐ´Đ¸ĐŊ} other {ĐŗĐžĐ´Đ¸ĐŊи}}", + "hours": "КоĐļĐŊ҃ {hours, plural, one {# ĐŗĐžĐ´Đ¸ĐŊ҃} few {# ĐŗĐžĐ´Đ¸ĐŊи} many {# ĐŗĐžĐ´Đ¸ĐŊ} other {# ĐŗĐžĐ´Đ¸ĐŊ}}", "night_at_midnight": "КоĐļĐŊĐžŅ— ĐŊĐžŅ‡Ņ– Đž ĐŋŅ–Đ˛ĐŊĐžŅ‡Ņ–", "night_at_twoam": "КоĐļĐŊĐžŅ— ĐŊĐžŅ‡Ņ– Đž 2:00" }, - "invalid_date": "НĐĩĐ´Ņ–ĐšŅĐŊа Đ´Đ°Ņ‚Đ°", - "invalid_date_format": "НĐĩĐ´Ņ–ĐšŅĐŊиК Ņ„ĐžŅ€ĐŧĐ°Ņ‚ Đ´Đ°Ņ‚Đ¸", - "invite_people": "ЗаĐŋŅ€ĐžŅĐ¸Ņ‚Đ¸", + "invalid_date": "НĐĩĐēĐžŅ€ĐĩĐēŅ‚ĐŊа Đ´Đ°Ņ‚Đ°", + "invalid_date_format": "НĐĩĐēĐžŅ€ĐĩĐēŅ‚ĐŊиК Ņ„ĐžŅ€ĐŧĐ°Ņ‚ Đ´Đ°Ņ‚Đ¸", + "invite_people": "ЗаĐŋŅ€ĐžŅĐ¸Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš", "invite_to_album": "ЗаĐŋŅ€ĐžŅĐ¸Ņ‚Đ¸ в аĐģŅŒĐąĐžĐŧ", "ios_debug_info_fetch_ran_at": "ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐž даĐŊŅ– {dateTime}", "ios_debug_info_last_sync_at": "ĐžŅŅ‚Đ°ĐŊĐŊŅ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ {dateTime}", "ios_debug_info_no_processes_queued": "ФОĐŊĐžĐ˛Ņ– ĐŋŅ€ĐžŅ†ĐĩŅĐ¸ Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ– в ҇ĐĩŅ€ĐˇŅ–", - "ios_debug_info_no_sync_yet": "ФОĐŊОвĐĩ СавдаĐŊĐŊŅ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ— ҉Đĩ ĐŊĐĩ СаĐŋ҃ҁĐēаĐģĐžŅŅ", + "ios_debug_info_no_sync_yet": "ФОĐŊОвĐĩ СавдаĐŊĐŊŅ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ— ҉Đĩ ĐŊĐĩ виĐēĐžĐŊŅƒĐ˛Đ°ĐģĐžŅŅ", "ios_debug_info_processes_queued": "{count, plural, one {{count} Ņ„ĐžĐŊОвиК ĐŋŅ€ĐžŅ†Đĩҁ ҃ ҇ĐĩŅ€ĐˇŅ–} few {{count} Ņ„ĐžĐŊĐžĐ˛Ņ– ĐŋŅ€ĐžŅ†ĐĩŅĐ¸ ҃ ҇ĐĩŅ€ĐˇŅ–} many {{count} Ņ„ĐžĐŊĐžĐ˛Đ¸Ņ… ĐŋŅ€ĐžŅ†ĐĩŅŅ–Đ˛ ҃ ҇ĐĩŅ€ĐˇŅ–} other {{count} Ņ„ĐžĐŊĐžĐ˛Đ¸Ņ… ĐŋŅ€ĐžŅ†ĐĩŅŅ–Đ˛ ҃ ҇ĐĩŅ€ĐˇŅ–}}", "ios_debug_info_processing_ran_at": "ĐžĐąŅ€ĐžĐąĐē҃ виĐēĐžĐŊаĐŊĐž {dateTime}", - "items_count": "{count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "items_count": "{count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", "jobs": "ЗавдаĐŊĐŊŅ", "json_editor": "JSON-Ņ€ĐĩдаĐēŅ‚ĐžŅ€", "json_error": "ПоĐŧиĐģĐēа JSON", "keep": "ЗаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸", - "keep_albums": "ЗбĐĩŅ€Ņ–ĐŗĐ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи", - "keep_albums_count": "ЗбĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ {count} {count, plural, one {аĐģŅŒĐąĐžĐŧ} few {аĐģŅŒĐąĐžĐŧи} many {аĐģŅŒĐąĐžĐŧŅ–Đ˛} other {аĐģŅŒĐąĐžĐŧŅ–Đ˛}}", - "keep_all": "ЗбĐĩŅ€ĐĩĐŗŅ‚Đ¸ Đ˛ŅĐĩ", + "keep_albums": "ЗаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи", + "keep_albums_count": "ЗаĐģĐ¸ŅˆĐ°Ņ”Ņ‚ŅŒŅŅ: {count} {count, plural, one {аĐģŅŒĐąĐžĐŧ} few {аĐģŅŒĐąĐžĐŧи} many {аĐģŅŒĐąĐžĐŧŅ–Đ˛} other {аĐģŅŒĐąĐžĐŧŅ–Đ˛}}", + "keep_all": "ЗаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ Đ˛ŅĐĩ", "keep_description": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ, Ņ‰Đž СаĐģĐ¸ŅˆĐ¸Ņ‚ŅŒŅŅ ĐŊа Đ˛Đ°ŅˆĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— ĐŋҖҁĐģŅ ĐˇĐ˛Ņ–ĐģҌĐŊĐĩĐŊĐŊŅ ĐŧŅ–ŅŅ†Ņ.", - "keep_favorites": "ЗбĐĩŅ€ĐĩĐŗŅ‚Đ¸ ĐžĐąŅ€Đ°ĐŊĐĩ", - "keep_on_device": "ЗбĐĩŅ€ĐĩĐŗŅ‚Đ¸ ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", + "keep_favorites": "ЗаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐĩ", + "keep_on_device": "ЗаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", "keep_on_device_hint": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸, ŅĐēŅ– ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ ĐŊа Ņ†ŅŒĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", - "keep_this_delete_others": "ЗаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ ҆ĐĩĐš Ņ„Đ°ĐšĐģ, видаĐģĐ¸Ņ‚Đ¸ Ņ–ĐŊŅˆŅ–", - "keeping": "ЗбĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ: {items}", - "kept_this_deleted_others": "ЗбĐĩŅ€ĐĩĐļĐĩĐŊĐž ҆ĐĩĐš Ņ„Đ°ĐšĐģ Ņ– видаĐģĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "keep_this_delete_others": "ЗаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ ҆ĐĩĐš ĐĩĐģĐĩĐŧĐĩĐŊŅ‚, видаĐģĐ¸Ņ‚Đ¸ Ņ–ĐŊŅˆŅ–", + "keeping": "ЗаĐģĐ¸ŅˆĐ°Ņ”Ņ‚ŅŒŅŅ: {items}", + "kept_this_deleted_others": "ЗбĐĩŅ€ĐĩĐļĐĩĐŊĐž ҆ĐĩĐš ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ Ņ– видаĐģĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", "keyboard_shortcuts": "ĐĄĐŋĐžĐģŅƒŅ‡ĐĩĐŊĐŊŅ ĐēĐģĐ°Đ˛Ņ–Ņˆ", "language": "Мова", "language_no_results_subtitle": "ĐĄĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋĐžŅˆŅƒĐēОвиК СаĐŋĐ¸Ņ‚", "language_no_results_title": "Мови ĐŊĐĩ СĐŊаКдĐĩĐŊĐž", - "language_search_hint": "ĐŸĐžŅˆŅƒĐē ĐŧОв...", + "language_search_hint": "ĐŸĐžŅˆŅƒĐē ĐŧОвâ€Ļ", "language_setting_description": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐŧĐžĐ˛Ņƒ, ŅĐēŅ–Đš ви ĐŊĐ°Đ´Đ°Ņ”Ņ‚Đĩ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ", "large_files": "ВĐĩĐģиĐēŅ– Ņ„Đ°ĐšĐģи", "last": "ĐžŅŅ‚Đ°ĐŊĐŊŅ–Đš", - "last_months": "{count, plural, one {МиĐŊ҃ĐģĐžĐŗĐž ĐŧŅ–ŅŅŅ†Ņ} few {ĐžŅŅ‚Đ°ĐŊĐŊŅ– # ĐŧŅ–ŅŅŅ†Ņ–} many {ĐžŅŅ‚Đ°ĐŊĐŊŅ– # ĐŧŅ–ŅŅŅ†Ņ–Đ˛} other {ĐžŅŅ‚Đ°ĐŊĐŊŅ– # ĐŧŅ–ŅŅŅ†Ņ–Đ˛}}", - "last_seen": "Đ’ĐžŅŅ‚Đ°ĐŊĐŊŅ” ĐąĐ°Ņ‡Đ¸Đģи", + "last_months": "{count, plural, one {ĐžŅŅ‚Đ°ĐŊĐŊŅ–Đš ĐŧŅ–ŅŅŅ†ŅŒ} few {ĐžŅŅ‚Đ°ĐŊĐŊŅ– # ĐŧŅ–ŅŅŅ†Ņ–} many {ĐžŅŅ‚Đ°ĐŊĐŊŅ– # ĐŧŅ–ŅŅŅ†Ņ–Đ˛} other {ĐžŅŅ‚Đ°ĐŊĐŊŅ– # ĐŧŅ–ŅŅŅ†Ņ–Đ˛}}", + "last_seen": "Đ’ĐžŅŅ‚Đ°ĐŊĐŊŅ” ĐŋĐžĐŧҖ҇ĐĩĐŊĐž", "latest_version": "ĐžŅŅ‚Đ°ĐŊĐŊŅ вĐĩŅ€ŅŅ–Ņ", "latitude": "Đ¨Đ¸Ņ€ĐžŅ‚Đ°", "leave": "ПоĐēиĐŊŅƒŅ‚Đ¸", - "leave_album": "Đ’Đ¸ĐšŅ‚Đ¸ С аĐģŅŒĐąĐžĐŧ҃", + "leave_album": "ПоĐēиĐŊŅƒŅ‚Đ¸ аĐģŅŒĐąĐžĐŧ", "lens_model": "МодĐĩĐģҌ Ой'Ņ”ĐēŅ‚Đ¸Đ˛Đ°", - "let_others_respond": "ДозвоĐģĐ¸Ņ‚Đ¸ Ņ–ĐŊŅˆĐ¸Đŧ Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°Ņ‚Đ¸", + "let_others_respond": "Đ”Đ°Ņ‚Đ¸ СĐŧĐžĐŗŅƒ Ņ–ĐŊŅˆĐ¸Đŧ Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°Ņ‚Đ¸", "level": "Đ Ņ–Đ˛ĐĩĐŊҌ", "library": "Đ‘Ņ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēа", "library_add_folder": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐŋаĐŋĐē҃", "library_edit_folder": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋаĐŋĐē҃", - "library_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", + "library_options": "Đ’Đ°Ņ€Ņ–Đ°ĐŊŅ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", "library_page_device_albums": "АĐģŅŒĐąĐžĐŧи ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", "library_page_new_album": "Новий аĐģŅŒĐąĐžĐŧ", - "library_page_sort_asset_count": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ Ņ„Đ°ĐšĐģŅ–Đ˛", - "library_page_sort_created": "НĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊĐž ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊŅ–", + "library_page_sort_asset_count": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", + "library_page_sort_created": "Đ”Đ°Ņ‚Đ° ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ", "library_page_sort_last_modified": "ĐžŅŅ‚Đ°ĐŊĐŊŅ СĐŧŅ–ĐŊа", "library_page_sort_title": "Назва аĐģŅŒĐąĐžĐŧ҃", "licenses": "Đ›Ņ–Ņ†ĐĩĐŊĐˇŅ–Ņ—", "light": "ĐĄĐ˛Ņ–Ņ‚Đģа", + "light_theme": "ПĐĩŅ€ĐĩĐŧĐēĐŊŅƒŅ‚Đ¸ ĐŊа ŅĐ˛Ņ–Ņ‚Đģ҃ Ņ‚ĐĩĐŧ҃", "like": "ĐŸĐžĐ´ĐžĐąĐ°Ņ”Ņ‚ŅŒŅŅ", "like_deleted": "ВĐŋОдОйаĐŊĐŊŅ видаĐģĐĩĐŊĐž", - "link_motion_video": "ĐŸĐžŅĐ¸ĐģаĐŊĐŊŅ ĐŊа Ņ€ŅƒŅ…ĐžĐŧĐĩ Đ˛Ņ–Đ´ĐĩĐž", - "link_to_oauth": "ĐŸŅ€Đ¸Ņ”Đ´ĐŊаĐŊĐŊŅ Đ´Đž OAuth", - "linked_oauth_account": "ĐŸŅ€Đ¸Đ˛'ŅĐˇĐ°ĐŊиК ОйĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ OAuth", - "list": "ПĐĩŅ€ĐĩĐģŅ–Đē", + "link_motion_video": "ĐŸŅ€Đ¸Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Ņ€ŅƒŅ…ĐžĐŧĐĩ Đ˛Ņ–Đ´ĐĩĐž", + "link_to_docs": "ДоĐēĐģадĐŊŅ–ŅˆĐĩ Đ´Đ¸Đ˛Ņ–Ņ‚ŅŒŅŅ в Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ–Ņ—.", + "link_to_oauth": "ĐŸŅ€Đ¸Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Đ´Đž OAuth", + "linked_oauth_account": "ĐŸŅ€Đ¸Ņ”Đ´ĐŊаĐŊиК ОйĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ OAuth", + "list": "ĐĄĐŋĐ¸ŅĐžĐē", "loading": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", "loading_search_results_failed": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚Đ¸ ĐŋĐžŅˆŅƒĐē҃", "local": "На ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", - "local_asset_cast_failed": "НĐĩĐŧĐžĐļĐģивО Ņ‚Ņ€Đ°ĐŊҁĐģŅŽĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģ, ŅĐēиК ĐŊĐĩ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€", + "local_asset_cast_failed": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Ņ‚Ņ€Đ°ĐŊҁĐģŅŽĐ˛Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚, ŅĐēиК ĐŊĐĩ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐž ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€", "local_assets": "ЛоĐēаĐģҌĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", - "local_id": "ĐœŅ–ŅŅ†ĐĩвиК Ņ–Đ´ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ĐžŅ€", - "local_media_summary": "ЗвĐĩĐ´ĐĩĐŊĐŊŅ ĐģĐžĐēаĐģҌĐŊĐ¸Ņ… ĐŧĐĩĐ´Ņ–Đ°Ņ„Đ°ĐšĐģŅ–Đ˛", + "local_id": "ЛоĐēаĐģҌĐŊиК Ņ–Đ´ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ĐžŅ€", + "local_media_summary": "ЗвĐĩĐ´ĐĩĐŊĐŊŅ ĐģĐžĐēаĐģҌĐŊĐ¸Ņ… ĐŧĐĩĐ´Ņ–Đ°", "local_network": "ЛоĐēаĐģҌĐŊа ĐŧĐĩŅ€ĐĩĐļа", - "local_network_sheet_info": "Đ—Đ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē ĐŋŅ–Đ´ĐēĐģŅŽŅ‡Đ°Ņ‚Đ¸ĐŧĐĩŅ‚ŅŒŅŅ Đ´Đž ҁĐĩŅ€Đ˛ĐĩŅ€Đ° ҇ĐĩŅ€ĐĩС ҆ĐĩĐš URL, ĐēĐžĐģи виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ вĐēаСаĐŊа Wi-Fi ĐŧĐĩŅ€ĐĩĐļа", - "location": "Đ ĐžĐˇŅ‚Đ°ŅˆŅƒĐ˛Đ°ĐŊĐŊŅ", - "location_permission": "Đ”ĐžĐˇĐ˛Ņ–Đģ Đ´Đž ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", - "location_permission_content": "ЊОй ĐŋĐĩŅ€ĐĩĐŧиĐēĐ°Ņ‚Đ¸ ĐŧĐĩŅ€ĐĩĐļŅ– ҃ Ņ„ĐžĐŊОвОĐŧ҃ Ņ€ĐĩĐļиĐŧŅ–, Immich ĐŧĐ°Ņ” СавĐļди ĐŧĐ°Ņ‚Đ¸ Đ´ĐžŅŅ‚ŅƒĐŋ Đ´Đž Ņ‚ĐžŅ‡ĐŊĐžŅ— ĐŗĐĩĐžĐģĐžĐēĐ°Ņ†Ņ–Ņ—, Ņ‰ĐžĐą ĐˇŅ‡Đ¸Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐ°ĐˇĐ˛Ņƒ Wi-Fi ĐŧĐĩŅ€ĐĩĐļŅ–", + "local_network_sheet_info": "Đ—Đ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē ĐŋŅ–Đ´'Ņ”Đ´ĐŊŅƒĐ˛Đ°Ņ‚Đ¸ĐŧĐĩŅ‚ŅŒŅŅ Đ´Đž ҁĐĩŅ€Đ˛ĐĩŅ€Đ° ҇ĐĩŅ€ĐĩС ҆ĐĩĐš URL, ĐēĐžĐģи виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ вĐēаСаĐŊа ĐŧĐĩŅ€ĐĩĐļа Wi-Fi", + "location": "ĐœŅ–ŅŅ†Đĩ", + "location_permission": "Đ”ĐžĐˇĐ˛Ņ–Đģ ĐŊа виСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŧŅ–ŅŅ†Ņ", + "location_permission_content": "ЊОй Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐĩ ĐŋĐĩŅ€ĐĩĐŧиĐēаĐŊĐŊŅ ĐŋŅ€Đ°Ņ†ŅŽĐ˛Đ°ĐģĐž, Immich ĐŋĐžŅ‚Ņ€ĐĩĐąŅƒŅ” дОСвОĐģ҃ ĐŊа Ņ‚ĐžŅ‡ĐŊĐĩ виСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŧŅ–ŅŅ†Ņ, Ņ‰ĐžĐą ĐˇŅ‡Đ¸Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐ°ĐˇĐ˛Ņƒ ĐŋĐžŅ‚ĐžŅ‡ĐŊĐžŅ— ĐŧĐĩŅ€ĐĩĐļŅ– Wi-Fi", "location_picker_choose_on_map": "ĐžĐąŅ€Đ°Ņ‚Đ¸ ĐŊа ĐŧаĐŋŅ–", "location_picker_latitude_error": "ВĐēаĐļŅ–Ņ‚ŅŒ Đ´Ņ–ĐšŅĐŊ҃ ŅˆĐ¸Ņ€ĐžŅ‚Ņƒ", "location_picker_latitude_hint": "ВĐēаĐļŅ–Ņ‚ŅŒ ŅˆĐ¸Ņ€ĐžŅ‚Ņƒ", @@ -1415,57 +1419,57 @@ "log_detail_title": "ДĐĩŅ‚Đ°ĐģŅ– ĐļŅƒŅ€ĐŊаĐģ҃", "log_out": "Đ’Đ¸ĐšŅ‚Đ¸", "log_out_all_devices": "Đ’Đ¸ĐšŅ‚Đ¸ С ŅƒŅŅ–Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—Đ˛", - "logged_in_as": "Đ’Ņ…Ņ–Đ´ виĐēĐžĐŊаĐŊĐž ŅĐē {user}", - "logged_out_all_devices": "Đ’Đ¸ĐšŅˆĐģи С ŅƒŅŅ–Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—Đ˛", - "logged_out_device": "Đ’Đ¸Ņ…Ņ–Đ´ С ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", + "logged_in_as": "Ви Đ˛Đ˛Ņ–ĐšŅˆĐģи ŅĐē {user}", + "logged_out_all_devices": "ВийдĐĩĐŊĐž С ŅƒŅŅ–Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—Đ˛", + "logged_out_device": "ВийдĐĩĐŊĐž С ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", "login": "Đ’Ņ…Ņ–Đ´", - "login_disabled": "ĐĐ˛Ņ‚ĐžŅ€Đ¸ĐˇĐ°Ņ†Ņ–ŅŽ виĐŧĐēĐŊĐĩĐŊĐž", + "login_disabled": "Đ’Ņ…Ņ–Đ´ виĐŧĐēĐŊĐĩĐŊĐž", "login_form_api_exception": "ПоĐŧиĐģĐēа API. ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ Đ°Đ´Ņ€Đĩҁ҃ ҁĐĩŅ€Đ˛ĐĩŅ€Đ° Ņ– ҁĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ СĐŊĐžĐ˛Ņƒ.", "login_form_back_button_text": "Назад", "login_form_email_hint": "youremail@email.com", "login_form_endpoint_hint": "http://your-server-ip:port", - "login_form_endpoint_url": "ĐĐ´Ņ€ĐĩŅĐ° ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ", + "login_form_endpoint_url": "ĐĐ´Ņ€ĐĩŅĐ° ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "login_form_err_http": "ВĐēаĐļŅ–Ņ‚ŅŒ http:// айО https://", "login_form_err_invalid_email": "НĐĩĐ´Ņ–ĐšŅĐŊа ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊа Đ°Đ´Ņ€ĐĩŅĐ°", "login_form_err_invalid_url": "НĐĩĐ´Ņ–ĐšŅĐŊиК URL", "login_form_err_leading_whitespace": "ĐŸŅ€ĐžĐąŅ–Đģ ĐŊа ĐŋĐžŅ‡Đ°Ņ‚Đē҃", - "login_form_err_trailing_whitespace": "ĐŸŅ€ĐžĐąŅ–Đģ в ĐēŅ–ĐŊ҆Җ", - "login_form_failed_get_oauth_server_config": "ПоĐŧиĐģĐēа Đ˛Ņ…ĐžĐ´Ņƒ ҇ĐĩŅ€ĐĩС OAuth, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ Đ°Đ´Ņ€Đĩҁ҃ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", + "login_form_err_trailing_whitespace": "ĐŸŅ€ĐžĐąŅ–Đģ ҃ ĐēŅ–ĐŊ҆Җ", + "login_form_failed_get_oauth_server_config": "НĐĩ вдаĐģĐžŅŅ Đ˛Đ˛Ņ–ĐšŅ‚Đ¸ ҇ĐĩŅ€ĐĩС OAuth, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ Đ°Đ´Ņ€Đĩҁ҃ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "login_form_failed_get_oauth_server_disable": "OAuth ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиК ĐŊа Ņ†ŅŒĐžĐŧ҃ ҁĐĩŅ€Đ˛ĐĩҀҖ", - "login_form_failed_login": "ПоĐŧиĐģĐēа Đ˛Ņ…ĐžĐ´Ņƒ, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ URL-Đ°Đ´Ņ€Đĩҁ҃ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°, ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊ҃ ĐŋĐžŅˆŅ‚Ņƒ Ņ‚Đ° ĐŋĐ°Ņ€ĐžĐģҌ", - "login_form_handshake_exception": "ПоĐŧиĐģĐēа Đ˛ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ С'Ņ”Đ´ĐŊаĐŊĐŊŅ С ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ. ĐŖĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐē҃ ŅĐ°ĐŧĐžĐŋŅ–Đ´ĐŋĐ¸ŅĐ°ĐŊĐžĐŗĐž ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ° в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ…, ŅĐēŅ‰Đž ви виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ ŅĐ°ĐŧĐžĐŋŅ–Đ´ĐŋĐ¸ŅĐ°ĐŊиК ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚.", + "login_form_failed_login": "НĐĩ вдаĐģĐžŅŅ Đ˛Đ˛Ņ–ĐšŅ‚Đ¸, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ URL-Đ°Đ´Ņ€Đĩҁ҃ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°, ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊ҃ ĐŋĐžŅˆŅ‚Ņƒ Ņ‚Đ° ĐŋĐ°Ņ€ĐžĐģҌ", + "login_form_handshake_exception": "НĐĩ вдаĐģĐžŅŅ ŅƒŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ С'Ņ”Đ´ĐŊаĐŊĐŊŅ Ņ–Đˇ ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ. ĐŖĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐē҃ ŅĐ°ĐŧĐžĐŋŅ–Đ´ĐŋĐ¸ŅĐ°ĐŊĐžĐŗĐž ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ° в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ…, ŅĐēŅ‰Đž ви виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ ŅĐ°ĐŧĐžĐŋŅ–Đ´ĐŋĐ¸ŅĐ°ĐŊиК ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚.", "login_form_password_hint": "ĐŋĐ°Ņ€ĐžĐģҌ", - "login_form_save_login": "ЗаĐŋаĐŧ'ŅŅ‚Đ°Ņ‚Đ¸ Đ˛Ņ…Ņ–Đ´", + "login_form_save_login": "ЗаĐģĐ¸ŅˆĐ°Ņ‚Đ¸ŅŅ ҃ ŅĐ¸ŅŅ‚ĐĩĐŧŅ–", "login_form_server_empty": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ URL-Đ°Đ´Ņ€Đĩҁ҃ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°.", - "login_form_server_error": "НĐĩ вдаĐģĐžŅŅ ĐŋŅ–Đ´ĐēĐģŅŽŅ‡Đ¸Ņ‚Đ¸ŅŅ Đ´Đž ҁĐĩŅ€Đ˛ĐĩŅ€Đ°.", + "login_form_server_error": "НĐĩ вдаĐģĐžŅŅ С'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ŅŅ Ņ–Đˇ ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ.", "login_has_been_disabled": "Đ’Ņ…Ņ–Đ´ ĐąŅƒĐģĐž виĐŧĐēĐŊĐĩĐŊĐž.", - "login_password_changed_error": "ПоĐŧиĐģĐēа ҃ ĐžĐŊОвĐģĐĩĐŊŅ– Đ˛Đ°ŅˆĐžĐŗĐž ĐŋĐ°Ņ€ĐžĐģŅ", - "login_password_changed_success": "ĐŸĐ°Ņ€ĐžĐģҌ ĐžĐŊОвĐģĐĩĐŊĐž ҃ҁĐŋŅ–ŅˆĐŊĐž", + "login_password_changed_error": "НĐĩ вдаĐģĐžŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", + "login_password_changed_success": "ĐŸĐ°Ņ€ĐžĐģҌ ĐžĐŊОвĐģĐĩĐŊĐž", "logout_all_device_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Đ˛Đ¸ĐšŅ‚Đ¸ С ŅƒŅŅ–Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—Đ˛?", "logout_this_device_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Đ˛Đ¸ĐšŅ‚Đ¸ С Ņ†ŅŒĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ?", "logs": "Đ–ŅƒŅ€ĐŊаĐģи", "longitude": "Đ”ĐžĐ˛ĐŗĐžŅ‚Đ°", "look": "Đ’Đ¸ĐŗĐģŅĐ´", - "loop_videos": "ĐĻиĐēĐģҖ҇ĐŊŅ– Đ˛Ņ–Đ´ĐĩĐž", - "loop_videos_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ†Đ¸ĐēĐģҖ҇ĐŊĐĩ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ Đ˛Ņ–Đ´ĐĩĐž.", + "loop_videos": "ĐŸĐžĐ˛Ņ‚ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ Đ˛Ņ–Đ´ĐĩĐž", + "loop_videos_description": "ВĐŧиĐēĐ°Ņ‚Đ¸ Ņ†Đ¸ĐēĐģҖ҇ĐŊĐĩ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ Đ˛Ņ–Đ´ĐĩĐž ĐŋŅ–Đ´ Ņ‡Đ°Ņ Đ´ĐĩŅ‚Đ°ĐģҌĐŊĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ.", "main_branch_warning": "Ви виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ вĐĩŅ€ŅŅ–ŅŽ Đ´ĐģŅ Ņ€ĐžĐˇŅ€ĐžĐąĐŊиĐēŅ–Đ˛; ĐŊĐ°ŅŅ‚Ņ–ĐšĐŊĐž Ņ€ĐĩĐēĐžĐŧĐĩĐŊĐ´ŅƒŅ”ĐŧĐž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ€ĐĩĐģŅ–ĐˇĐŊ҃ вĐĩŅ€ŅŅ–ŅŽ!", "main_menu": "ГоĐģОвĐŊĐĩ ĐŧĐĩĐŊŅŽ", "maintenance_action_restore": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ йаСи даĐŊĐ¸Ņ…", - "maintenance_description": "Immich ĐŋĐĩŅ€ĐĩвĐĩĐ´ĐĩĐŊĐž в Ņ€ĐĩĐļиĐŧ Ņ‚ĐĩŅ…ĐŊҖ҇ĐŊĐžĐŗĐž ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ.", - "maintenance_end": "ЗавĐĩŅ€ŅˆĐ¸Ņ‚Đ¸ Ņ€ĐĩĐļиĐŧ Ņ‚ĐĩŅ…ĐŊҖ҇ĐŊĐžĐŗĐž ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ", + "maintenance_description": "Immich ĐŋĐĩŅ€ĐĩвĐĩĐ´ĐĩĐŊĐž в Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ.", + "maintenance_end": "ЗавĐĩŅ€ŅˆĐ¸Ņ‚Đ¸ Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ", "maintenance_end_error": "НĐĩ вдаĐģĐžŅŅ СавĐĩŅ€ŅˆĐ¸Ņ‚Đ¸ Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ.", "maintenance_logged_in_as": "ĐĐ°Ņ€Đ°ĐˇŅ– ви Đ˛Đ˛Ņ–ĐšŅˆĐģи ŅĐē {user}", "maintenance_restore_from_backup": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ С Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ—", "maintenance_restore_library": "Đ’Ņ–Đ´ĐŊĐžĐ˛Ņ–Ņ‚ŅŒ ŅĐ˛ĐžŅŽ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", - "maintenance_restore_library_confirm": "Đ¯ĐēŅ‰Đž ҆Đĩ Đ˛Đ¸ĐŗĐģŅĐ´Đ°Ņ” ĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊĐž, ĐŋŅ€ĐžĐ´ĐžĐ˛ĐļŅƒĐšŅ‚Đĩ Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ—!", + "maintenance_restore_library_confirm": "Đ¯ĐēŅ‰Đž Đ˛ŅĐĩ ĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊĐž, ĐŋŅ€ĐžĐ´ĐžĐ˛ĐļŅƒĐšŅ‚Đĩ Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ—!", "maintenance_restore_library_description": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ йаСи даĐŊĐ¸Ņ…", - "maintenance_restore_library_folder_has_files": "{folder} ĐŧĐ°Ņ” {count} ĐŋаĐŋĐžĐē(ĐžĐē)", + "maintenance_restore_library_folder_has_files": "{folder} ĐŧĐ°Ņ” {count, plural, one {# ĐŋаĐŋĐē҃} few {# ĐŋаĐŋĐēи} many {# ĐŋаĐŋĐžĐē} other {# ĐŋаĐŋĐžĐē}}", "maintenance_restore_library_folder_no_files": "ĐŖ ĐŋаĐŋ҆Җ {folder} Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ– Ņ„Đ°ĐšĐģи!", - "maintenance_restore_library_folder_pass": "Ņ‡Đ¸Ņ‚Đ°ĐąĐĩĐģҌĐŊиК Ņ‚Đ° СаĐŋĐ¸ŅŅƒĐ˛Đ°ĐŊиК", - "maintenance_restore_library_folder_read_fail": "ĐŊĐĩŅ‡Đ¸Ņ‚Đ°ĐąĐĩĐģҌĐŊĐž", - "maintenance_restore_library_folder_write_fail": "ĐŊĐĩ ĐŧĐžĐļĐŊа СаĐŋĐ¸ŅŅƒĐ˛Đ°Ņ‚Đ¸", - "maintenance_restore_library_hint_missing_files": "МоĐļĐģивО, ви ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°Ņ”Ņ‚Đĩ ваĐļĐģĐ¸Đ˛Ņ– Ņ„Đ°ĐšĐģи", - "maintenance_restore_library_hint_regenerate_later": "Ви ĐŧĐžĐļĐĩŅ‚Đĩ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ—Ņ… ĐŋŅ–ĐˇĐŊŅ–ŅˆĐĩ в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ…", + "maintenance_restore_library_folder_pass": "Đ´ĐžŅŅ‚ŅƒĐŋĐŊа Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ Ņ‚Đ° СаĐŋĐ¸ŅŅƒ", + "maintenance_restore_library_folder_read_fail": "ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ", + "maintenance_restore_library_folder_write_fail": "ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа Đ´ĐģŅ СаĐŋĐ¸ŅŅƒ", + "maintenance_restore_library_hint_missing_files": "МоĐļĐģивО, ҃ Đ˛Đ°Ņ Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ– ваĐļĐģĐ¸Đ˛Ņ– Ņ„Đ°ĐšĐģи", + "maintenance_restore_library_hint_regenerate_later": "Ви ĐŧĐžĐļĐĩŅ‚Đĩ ĐŋĐĩŅ€ĐĩŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Ņ—Ņ… ĐŋŅ–ĐˇĐŊŅ–ŅˆĐĩ в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ…", "maintenance_restore_library_hint_storage_template_missing_files": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ ŅˆĐ°ĐąĐģĐžĐŊ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°? МоĐļĐģивО, ваĐŧ ĐąŅ€Đ°ĐēŅƒŅ” Ņ„Đ°ĐšĐģŅ–Đ˛", "maintenance_restore_library_loading": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€ĐžĐē ҆ҖĐģҖҁĐŊĐžŅŅ‚Ņ– Ņ‚Đ° ĐĩĐ˛Ņ€Đ¸ŅŅ‚Đ¸Đēâ€Ļ", "maintenance_task_backup": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ— ҖҁĐŊŅƒŅŽŅ‡ĐžŅ— йаСи даĐŊĐ¸Ņ…â€Ļ", @@ -1474,102 +1478,102 @@ "maintenance_task_rollback": "НĐĩ вдаĐģĐžŅŅ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸, ĐŋОвĐĩŅ€ĐŊĐĩĐŊĐŊŅ Đ´Đž Ņ‚ĐžŅ‡Đēи Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅâ€Ļ", "maintenance_title": "ĐĸиĐŧŅ‡Đ°ŅĐžĐ˛Đž ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊĐž", "make": "Đ’Đ¸Ņ€ĐžĐąĐŊиĐē", - "manage_geolocation": "КĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅĐŧ", - "manage_media_access_rationale": "ĐĻĐĩĐš Đ´ĐžĐˇĐ˛Ņ–Đģ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐĩĐŊ Đ´ĐģŅ ĐŊаĐģĐĩĐļĐŊĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐŊŅ Ņ„Đ°ĐšĐģŅ–Đ˛ Đ´Đž ĐēĐžŅˆĐ¸Đēа Ņ‚Đ° Ņ—Ņ… Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ С ĐŊŅŒĐžĐŗĐž.", + "manage_geolocation": "КĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧҖҁ҆ĐĩĐŧ", + "manage_media_access_rationale": "ĐĻĐĩĐš Đ´ĐžĐˇĐ˛Ņ–Đģ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐĩĐŊ, Ņ‰ĐžĐą ĐŊаĐģĐĩĐļĐŊĐž ĐŋĐĩŅ€ĐĩĐŧŅ–Ņ‰ŅƒĐ˛Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ Đ´Đž ĐēĐžŅˆĐ¸Đēа Ņ‚Đ° Đ˛Ņ–Đ´ĐŊОвĐģŅŽĐ˛Đ°Ņ‚Đ¸ Ņ—Ņ… С ĐŊŅŒĐžĐŗĐž.", "manage_media_access_settings": "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", - "manage_media_access_subtitle": "ДозвоĐģŅŒŅ‚Đĩ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ Immich ĐēĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧĐĩĐ´Ņ–Đ°Ņ„Đ°ĐšĐģаĐŧи Ņ‚Đ° ĐŋĐĩŅ€ĐĩĐŧŅ–Ņ‰ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ—Ņ….", + "manage_media_access_subtitle": "Đ”Đ°ĐšŅ‚Đĩ СĐŧĐžĐŗŅƒ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ Immich ĐēĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧĐĩĐ´Ņ–Đ°Ņ„Đ°ĐšĐģаĐŧи Ņ‚Đ° ĐŋĐĩŅ€ĐĩĐŧŅ–Ņ‰ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ—Ņ….", "manage_media_access_title": "Đ”ĐžŅŅ‚ŅƒĐŋ Đ´Đž ĐēĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧĐĩĐ´Ņ–Đ°", "manage_shared_links": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐŋŅ–ĐģҌĐŊиĐŧи ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧи", - "manage_sharing_with_partners": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐŋŅ–ĐģҌĐŊиĐŧ Đ´ĐžŅŅ‚ŅƒĐŋĐžĐŧ С ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°Đŧи", + "manage_sharing_with_partners": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐŋŅ–ĐģҌĐŊиĐŧ Đ´ĐžŅŅ‚ŅƒĐŋĐžĐŧ Ņ–Đˇ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°Đŧи", "manage_the_app_settings": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃", "manage_your_account": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ОйĐģŅ–ĐēОвиĐŧ СаĐŋĐ¸ŅĐžĐŧ", "manage_your_api_keys": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐēĐģŅŽŅ‡Đ°Đŧи API", "manage_your_devices": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ Đ°Đ˛Ņ‚ĐžŅ€Đ¸ĐˇĐžĐ˛Đ°ĐŊиĐŧи ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅĐŧи", - "manage_your_oauth_connection": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋŅ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊĐžĐŗĐž OAuth", + "manage_your_oauth_connection": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋŅ–Đ´'Ņ”Đ´ĐŊаĐŊĐŊŅĐŧ OAuth", "map": "МаĐŋа", - "map_assets_in_bounds": "{count, plural, =0 {НĐĩĐŧĐ°Ņ” Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš ҃ Ņ†Ņ–Đš ĐŧҖҁ҆ĐĩĐ˛ĐžŅŅ‚Ņ–} one {# Ņ„ĐžŅ‚Đž} few {# Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—} many {# Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš} other {# Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš}}", - "map_cannot_get_user_location": "НĐĩ ĐŧĐžĐļ҃ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", + "map_assets_in_bounds": "{count, plural, =0 {НĐĩĐŧĐ°Ņ” Ņ„ĐžŅ‚Đž ҃ Ņ†Ņ–Đš ĐŧҖҁ҆ĐĩĐ˛ĐžŅŅ‚Ņ–} one {# Ņ„ĐžŅ‚Đž} few {# Ņ„ĐžŅ‚Đž} many {# Ņ„ĐžŅ‚Đž} other {# Ņ„ĐžŅ‚Đž}}", + "map_cannot_get_user_location": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐŧҖҁ҆Đĩ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "map_location_dialog_yes": "ĐĸаĐē", - "map_location_picker_page_use_location": "ВиĐēĐžŅ€Đ¸ŅŅ‚Đ°Ņ‚Đ¸ ҆Đĩ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", - "map_location_service_disabled_content": "ĐĄĐģ҃Đļйа ĐŗĐĩĐžĐģĐžĐēĐ°Ņ†Ņ–Ņ— ĐŧĐ°Ņ” ĐąŅƒŅ‚Đ¸ Đ˛Đ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐžŅŽ, Ņ‰ĐžĐą Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋĐžŅ‚ĐžŅ‡ĐŊĐžĐŗĐž ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ. ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ—Ņ— ĐˇĐ°Ņ€Đ°Đˇ?", - "map_location_service_disabled_title": "ĐĄĐģ҃Đļйа ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ виĐŧĐēĐŊĐĩĐŊа", - "map_marker_for_images": "ĐœĐ°Ņ€ĐēĐĩŅ€ ĐŊа ĐŧаĐŋŅ– Đ´ĐģŅ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ, ĐˇŅ€ĐžĐąĐģĐĩĐŊĐ¸Ņ… ҃ ĐŧҖҁ҂Җ {city}, {country}", + "map_location_picker_page_use_location": "ВиĐēĐžŅ€Đ¸ŅŅ‚Đ°Ņ‚Đ¸ ҆Đĩ ĐŧҖҁ҆Đĩ", + "map_location_service_disabled_content": "ĐĄĐģ҃Đļйа виСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŧŅ–ŅŅ†Ņ ĐŧĐ°Ņ” ĐąŅƒŅ‚Đ¸ ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊа, Ņ‰ĐžĐą Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋĐžŅ‚ĐžŅ‡ĐŊĐžĐŗĐž ĐŧŅ–ŅŅ†Ņ. ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ—Ņ— ĐˇĐ°Ņ€Đ°Đˇ?", + "map_location_service_disabled_title": "ĐĄĐģ҃Đļйа виСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŧŅ–ŅŅ†Ņ виĐŧĐēĐŊĐĩĐŊа", + "map_marker_for_images": "ĐœĐ°Ņ€ĐēĐĩŅ€ ĐŊа ĐŧаĐŋŅ– Đ´ĐģŅ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ, СĐŊŅŅ‚Đ¸Ņ… ҃ {city}, {country}", "map_marker_with_image": "ĐœĐ°Ņ€ĐēĐĩŅ€ ĐŊа ĐŧаĐŋŅ– Ņ–Đˇ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅĐŧ", - "map_no_location_permission_content": "ĐŸĐžŅ‚Ņ€Ņ–ĐąĐĩĐŊ Đ´ĐžĐˇĐ˛Ņ–Đģ, айи ĐŋĐžĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи Ņ–Đˇ ĐŋĐžŅ‚ĐžŅ‡ĐŊĐžĐŗĐž ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ. ĐĐ°Đ´Đ°Ņ‚Đ¸ ĐšĐžĐŗĐž ĐˇĐ°Ņ€Đ°Đˇ?", - "map_no_location_permission_title": "ПоĐŧиĐģĐēа Đ´ĐžŅŅ‚ŅƒĐŋ҃ Đ´Đž ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", + "map_no_location_permission_content": "ĐŸĐžŅ‚Ņ€Ņ–ĐąĐĩĐŊ Đ´ĐžĐˇĐ˛Ņ–Đģ, Ņ‰ĐžĐą ĐŋĐžĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ Ņ–Đˇ ĐŋĐžŅ‚ĐžŅ‡ĐŊĐžĐŗĐž ĐŧŅ–ŅŅ†Ņ. ĐĐ°Đ´Đ°Ņ‚Đ¸ ĐšĐžĐŗĐž ĐˇĐ°Ņ€Đ°Đˇ?", + "map_no_location_permission_title": "Đ”ĐžŅŅ‚ŅƒĐŋ Đ´Đž ĐŧŅ–ŅŅ†Ņ ĐŊĐĩ ĐŊадаĐŊĐž", "map_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧаĐŋи", "map_settings_dark_mode": "ĐĸĐĩĐŧĐŊиК Ņ€ĐĩĐļиĐŧ", - "map_settings_date_range_option_day": "МиĐŊ҃ĐģŅ– 24 ĐŗĐžĐ´Đ¸ĐŊи", - "map_settings_date_range_option_days": "МиĐŊ҃ĐģĐ¸Ņ… {days} Đ´ĐŊŅ–Đ˛", - "map_settings_date_range_option_year": "МиĐŊ҃ĐģиК ҀҖĐē", + "map_settings_date_range_option_day": "За ĐžŅŅ‚Đ°ĐŊĐŊŅ– 24 ĐŗĐžĐ´Đ¸ĐŊи", + "map_settings_date_range_option_days": "За ĐžŅŅ‚Đ°ĐŊĐŊŅ– {days} Đ´ĐŊŅ–Đ˛", + "map_settings_date_range_option_year": "За ĐžŅŅ‚Đ°ĐŊĐŊŅ–Đš ҀҖĐē", "map_settings_date_range_option_years": "МиĐŊ҃ĐģŅ– {years} Ņ€ĐžĐēи", "map_settings_dialog_title": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧаĐŋи", - "map_settings_include_show_archived": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ‚Đ¸ Đ°Ņ€Ņ…Ņ–Đ˛", - "map_settings_include_show_partners": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°", - "map_settings_only_show_favorites": "Đ›Đ¸ŅˆĐĩ ĐžĐąŅ€Đ°ĐŊŅ–", + "map_settings_include_show_archived": "Đ’Ņ€Đ°Ņ…ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Đ°Ņ€Ņ…Ņ–Đ˛", + "map_settings_include_show_partners": "Đ’Ņ€Đ°Ņ…ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Ņ–Đ˛", + "map_settings_only_show_favorites": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ‚Đ¸ ĐģĐ¸ŅˆĐĩ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐĩ", "map_settings_theme_settings": "ĐĸĐĩĐŧа ĐŧаĐŋи", "map_zoom_to_see_photos": "ЗĐŧĐĩĐŊŅˆŅ‚Đĩ ĐŧĐ°ŅŅˆŅ‚Đ°Đą, Ņ‰ĐžĐą ĐŋĐžĐąĐ°Ņ‡Đ¸Ņ‚Đ¸ Ņ„ĐžŅ‚Đž", "mark_all_as_read": "ПозĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ Đ˛ŅŅ– ŅĐē ĐŋŅ€ĐžŅ‡Đ¸Ņ‚Đ°ĐŊŅ–", "mark_as_read": "ПозĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ŅĐē ĐŋŅ€ĐžŅ‡Đ¸Ņ‚Đ°ĐŊĐĩ", "marked_all_as_read": "ПозĐŊĐ°Ņ‡ĐĩĐŊĐž Đ˛ŅŅ– ŅĐē ĐŋŅ€ĐžŅ‡Đ¸Ņ‚Đ°ĐŊŅ–", "matches": "Đ—ĐąŅ–ĐŗĐ¸", - "matching_assets": "Đ’Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´ĐŊŅ– Ņ„Đ°ĐšĐģи", + "matching_assets": "Đ’Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´ĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", "media_type": "ĐĸиĐŋ ĐŧĐĩĐ´Ņ–Đ°", "memories": "ĐĄĐŋĐžĐŗĐ°Đ´Đ¸", "memories_all_caught_up": "ĐĻĐĩ Đ˛ŅĐĩ ĐŊа ŅŅŒĐžĐŗĐžĐ´ĐŊŅ–", "memories_check_back_tomorrow": "Đ—Đ°Đ˛Ņ–Ņ‚Đ°ĐšŅ‚Đĩ ĐˇĐ°Đ˛Ņ‚Ņ€Đ°, Ņ‰ĐžĐą ĐŋĐžĐąĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ ҁĐŋĐžĐŗĐ°Đ´Ņ–Đ˛", - "memories_setting_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ вĐŧŅ–ŅŅ‚Ņƒ ҁĐŋĐžĐŗĐ°Đ´Ņ–Đ˛", - "memories_start_over": "ĐŸĐžŅ‡Đ°Ņ‚Đ¸ СаĐŊОвО", + "memories_setting_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ вĐŧŅ–ŅŅ‚ĐžĐŧ ҁĐŋĐžĐŗĐ°Đ´Ņ–Đ˛", + "memories_start_over": "ĐŸĐžŅ‡Đ°Ņ‚Đ¸ СĐŊĐžĐ˛Ņƒ", "memories_swipe_to_close": "ЗĐŧĐ°Ņ…ĐŊŅ–Ņ‚ŅŒ Đ˛ĐŗĐžŅ€Ņƒ, Ņ‰ĐžĐą СаĐēŅ€Đ¸Ņ‚Đ¸", "memory": "ĐĄĐŋĐžĐŗĐ°Đ´", - "memory_lane_title": "АĐģĐĩŅ ĐĄĐŋĐžĐŗĐ°Đ´Ņ–Đ˛ {title}", + "memory_lane_title": "АĐģĐĩŅ ҁĐŋĐžĐŗĐ°Đ´Ņ–Đ˛ {title}", "menu": "МĐĩĐŊŅŽ", "merge": "Об'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸", "merge_people": "Об'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš", "merge_people_limit": "Ви ĐŧĐžĐļĐĩŅ‚Đĩ Ой'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Đ´Đž 5 ОйĐģĐ¸Ņ‡ ОдĐŊĐžŅ‡Đ°ŅĐŊĐž", "merge_people_prompt": "Ви Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Ой'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Ņ†Đ¸Ņ… ĐģŅŽĐ´ĐĩĐš? ĐĻŅ Đ´Ņ–Ņ ĐŊĐĩĐˇĐ˛ĐžŅ€ĐžŅ‚ĐŊа.", - "merge_people_successfully": "ĐŖŅĐŋŅ–ŅˆĐŊĐĩ Ой'Ņ”Đ´ĐŊаĐŊĐŊŅ ĐģŅŽĐ´ĐĩĐš", - "merged_people_count": "Об'Ņ”Đ´ĐŊаĐŊĐž {count, plural, one {# ĐžŅĐžĐąĐ°} few {# ĐžŅĐžĐąĐ¸} many {# ĐžŅŅ–Đą} other {# ĐģŅŽĐ´ĐĩĐš}}", - "minimize": "ĐœŅ–ĐŊŅ–ĐŧŅ–ĐˇŅƒĐ˛Đ°Ņ‚Đ¸", + "merge_people_successfully": "Đ›ŅŽĐ´ĐĩĐš Ой'Ņ”Đ´ĐŊаĐŊĐž", + "merged_people_count": "Об'Ņ”Đ´ĐŊаĐŊĐž {count, plural, one {# ĐģŅŽĐ´Đ¸ĐŊ҃} few {# ĐģŅŽĐ´Đ¸ĐŊи} many {# ĐģŅŽĐ´ĐĩĐš} other {# ĐģŅŽĐ´ĐĩĐš}}", + "minimize": "Đ—ĐŗĐžŅ€ĐŊŅƒŅ‚Đ¸", "minute": "ĐĨвиĐģиĐŊа", "minutes": "ĐĨвиĐģиĐŊи", - "mirror_horizontal": "Đ“ĐžŅ€Đ¸ĐˇĐžĐŊŅ‚Đ°ĐģҌĐŊиК", - "mirror_vertical": "ВĐĩŅ€Ņ‚Đ¸ĐēаĐģҌĐŊиК", + "mirror_horizontal": "По ĐŗĐžŅ€Đ¸ĐˇĐžĐŊŅ‚Đ°ĐģŅ–", + "mirror_vertical": "По вĐĩŅ€Ņ‚Đ¸ĐēаĐģŅ–", "missing": "Đ’Ņ–Đ´ŅŅƒŅ‚ĐŊŅ–", "mobile_app": "ĐœĐžĐąŅ–ĐģҌĐŊиК ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē", - "mobile_app_download_onboarding_note": "ЗаваĐŊŅ‚Đ°ĐļŅ‚Đĩ ҁ҃ĐŋŅƒŅ‚ĐŊŅ–Đš ĐŧĐžĐąŅ–ĐģҌĐŊиК ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē, ҁĐēĐžŅ€Đ¸ŅŅ‚Đ°Đ˛ŅˆĐ¸ŅŅŒ ĐŊавĐĩĐ´ĐĩĐŊиĐŧи ĐŊиĐļ҇Đĩ ĐžĐŋŅ†Ņ–ŅĐŧи", + "mobile_app_download_onboarding_note": "ЗаваĐŊŅ‚Đ°ĐļŅ‚Đĩ ĐŧĐžĐąŅ–ĐģҌĐŊиК ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē ОдĐŊиĐŧ Ņ–Đˇ ĐŊавĐĩĐ´ĐĩĐŊĐ¸Ņ… ĐŊиĐļ҇Đĩ ҁĐŋĐžŅĐžĐąŅ–Đ˛", "model": "МодĐĩĐģҌ", "month": "ĐœŅ–ŅŅŅ†ŅŒ", - "monthly_title_text_date_format": "ММММ Ņ€", + "monthly_title_text_date_format": "MMMM y", "more": "Đ‘Ņ–ĐģҌ҈Đĩ", "move": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸", "move_down": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ вĐŊиС", - "move_off_locked_folder": "Đ’Đ¸ĐšŅ‚Đ¸ С ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи", + "move_off_locked_folder": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ С ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи", "move_to": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ Đ´Đž", - "move_to_device_trash": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ в ĐēĐžŅˆĐ¸Đē ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", - "move_to_lock_folder_action_prompt": "{count} дОдаĐŊĐž Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи", + "move_to_device_trash": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ Đ´Đž ĐēĐžŅˆĐ¸Đēа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", + "move_to_lock_folder_action_prompt": "{count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ дОдаĐŊĐž Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ дОдаĐŊĐž Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ дОдаĐŊĐž Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ дОдаĐŊĐž Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи}}", "move_to_locked_folder": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи", - "move_to_locked_folder_confirmation": "ĐĻŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ĐąŅƒĐ´Đĩ видаĐģĐĩĐŊĐž ĐˇŅ– Đ˛ŅŅ–Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛ Ņ– Ņ—Ņ… ĐŧĐžĐļĐŊа ĐąŅƒĐ´Đĩ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‚Đ¸ ĐģĐ¸ŅˆĐĩ в ĐžŅĐžĐąĐ¸ŅŅ‚Ņ–Đš ĐŋаĐŋ҆Җ", + "move_to_locked_folder_confirmation": "ĐĻŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ĐąŅƒĐ´Đĩ виĐģŅƒŅ‡ĐĩĐŊĐž С ŅƒŅŅ–Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛ Ņ– Ņ—Ņ… ĐŧĐžĐļĐŊа ĐąŅƒĐ´Đĩ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‚Đ¸ ĐģĐ¸ŅˆĐĩ в ĐžŅĐžĐąĐ¸ŅŅ‚Ņ–Đš ĐŋаĐŋ҆Җ", "move_up": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ Đ˛ĐŗĐžŅ€Ņƒ", - "moved_to_archive": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} в Đ°Ņ€Ņ…Ņ–Đ˛", - "moved_to_library": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} в ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", + "moved_to_archive": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} Đ´Đž Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ", + "moved_to_library": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} Đ´Đž ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", "moved_to_trash": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа", - "multiselect_grid_edit_date_time_err_read_only": "НĐĩĐŧĐžĐļĐģивО Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ Ņ„Đ°ĐšĐģŅ–Đ˛ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", - "multiselect_grid_edit_gps_err_read_only": "НĐĩĐŧĐžĐļĐģивО Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐŗĐĩĐžĐģĐžĐēĐ°Ņ†Ņ–ŅŽ Ņ„Đ°ĐšĐģŅ–Đ˛ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", - "mute_memories": "ĐŸŅ€Đ¸ĐŗĐģŅƒŅˆĐ¸Ņ‚Đ¸ ҁĐŋĐžĐŗĐ°Đ´Đ¸", + "multiselect_grid_edit_date_time_err_read_only": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "multiselect_grid_edit_gps_err_read_only": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧҖҁ҆Đĩ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "mute_memories": "ВиĐŧĐēĐŊŅƒŅ‚Đ¸ ҁĐŋĐžĐŗĐ°Đ´Đ¸", "my_albums": "ĐœĐžŅ— аĐģŅŒĐąĐžĐŧи", "name": "ІĐŧ'Ņ", "name_or_nickname": "ІĐŧ'Ņ айО ĐŋҁĐĩвдОĐŊŅ–Đŧ", "name_required": "ІĐŧ'Ņ ОйОв'ŅĐˇĐēОвĐĩ", "navigate": "ĐĐ°Đ˛Ņ–ĐŗĐ°Ņ†Ņ–Ņ", - "navigate_to_time": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž Đ§Đ°ŅŅƒ", - "network_requirement_photos_upload": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ҁ҂ҖĐģҌĐŊиĐēĐžĐ˛Ņ– даĐŊŅ– Đ´ĐģŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Ņ„ĐžŅ‚Đž", - "network_requirement_videos_upload": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ҁ҂ҖĐģҌĐŊиĐēĐžĐ˛Ņ– даĐŊŅ– Đ´ĐģŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Đ˛Ņ–Đ´ĐĩĐž", + "navigate_to_time": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž Ņ‡Đ°ŅŅƒ", + "network_requirement_photos_upload": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧĐžĐąŅ–ĐģҌĐŊŅ– даĐŊŅ– Đ´ĐģŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Ņ„ĐžŅ‚Đž", + "network_requirement_videos_upload": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧĐžĐąŅ–ĐģҌĐŊŅ– даĐŊŅ– Đ´ĐģŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Đ˛Ņ–Đ´ĐĩĐž", "network_requirements": "ВиĐŧĐžĐŗĐ¸ Đ´Đž ĐŧĐĩŅ€ĐĩĐļŅ–", - "network_requirements_updated": "ВиĐŧĐžĐŗĐ¸ Đ´Đž ĐŧĐĩŅ€ĐĩĐļŅ– СĐŧŅ–ĐŊиĐģĐ¸ŅŅ, ҇ĐĩŅ€ĐŗĐ° Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ĐžŅ‡Đ¸Ņ‰ĐĩĐŊа", - "networking_settings": "МĐĩŅ€ĐĩĐļĐĩĐ˛Ņ– ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", - "networking_subtitle": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧ Đ°Đ´Ņ€ĐĩŅĐ¸ ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ", - "never": "ĐŊŅ–ĐēĐžĐģи", + "network_requirements_updated": "ВиĐŧĐžĐŗĐ¸ Đ´Đž ĐŧĐĩŅ€ĐĩĐļŅ– СĐŧŅ–ĐŊĐĩĐŊĐž, ҇ĐĩŅ€ĐŗŅƒ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ҁĐēиĐŊŅƒŅ‚Đž", + "networking_settings": "МĐĩŅ€ĐĩĐļа", + "networking_subtitle": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи Đ°Đ´Ņ€ĐĩŅĐ¸ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", + "never": "ĐŅ–ĐēĐžĐģи", "new_album": "Новий аĐģŅŒĐąĐžĐŧ", "new_api_key": "Новий ĐēĐģŅŽŅ‡ API", "new_date_range": "Новий Đ´Ņ–Đ°ĐŋаСОĐŊ Đ´Đ°Ņ‚", @@ -1585,36 +1589,36 @@ "next": "ДаĐģŅ–", "next_memory": "ĐĐ°ŅŅ‚ŅƒĐŋĐŊиК ҁĐŋĐžĐŗĐ°Đ´", "no": "ĐŅ–", - "no_actions_added": "ПоĐēи Ņ‰Đž ĐļОдĐŊĐ¸Ņ… Đ´Ņ–Đš ĐŊĐĩ дОдаĐŊĐž", + "no_actions_added": "Đ”Ņ–Đš ҉Đĩ ĐŊĐĩ дОдаĐŊĐž", "no_albums_found": "АĐģŅŒĐąĐžĐŧи ĐŊĐĩ СĐŊаКдĐĩĐŊĐž", - "no_albums_message": "ĐĄŅ‚Đ˛ĐžŅ€Ņ–Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧ, Ņ‰ĐžĐą ҃ĐŋĐžŅ€ŅĐ´ĐēŅƒĐ˛Đ°Ņ‚Đ¸ ŅĐ˛ĐžŅ— Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", - "no_albums_with_name_yet": "ĐĄŅ…ĐžĐļĐĩ, ҃ Đ˛Đ°Ņ ҉Đĩ ĐŊĐĩĐŧĐ°Ņ” аĐģŅŒĐąĐžĐŧŅ–Đ˛ С Ņ‚Đ°ĐēĐžŅŽ ĐŊĐ°ĐˇĐ˛ĐžŅŽ.", + "no_albums_message": "ĐĄŅ‚Đ˛ĐžŅ€Ņ–Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧ, Ņ‰ĐžĐą ҃ĐŋĐžŅ€ŅĐ´ĐēŅƒĐ˛Đ°Ņ‚Đ¸ ŅĐ˛ĐžŅ— Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", + "no_albums_with_name_yet": "ĐĄŅ…ĐžĐļĐĩ, ҃ Đ˛Đ°Ņ ҉Đĩ ĐŊĐĩĐŧĐ°Ņ” аĐģŅŒĐąĐžĐŧŅ–Đ˛ Ņ–Đˇ Ņ‚Đ°ĐēĐžŅŽ ĐŊĐ°ĐˇĐ˛ĐžŅŽ.", "no_albums_yet": "ĐĄŅ…ĐžĐļĐĩ, ҃ Đ˛Đ°Ņ ҉Đĩ ĐŊĐĩĐŧĐ°Ņ” ĐļОдĐŊĐžĐŗĐž аĐģŅŒĐąĐžĐŧ҃.", - "no_archived_assets_message": "Đ—Đ°Đ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž, Ņ‰ĐžĐą ĐŋŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ Ņ—Ņ… ҃ Đ˛Đ°ŅˆĐžĐŧ҃ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņ– Ņ„ĐžŅ‚Đž", - "no_assets_message": "ĐĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ, Ņ‰ĐžĐą СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ŅĐ˛ĐžŅ” ĐŋĐĩŅ€ŅˆĐĩ Ņ„ĐžŅ‚Đž", - "no_assets_to_show": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ–", + "no_archived_assets_message": "ĐŅ€Ņ…Ņ–Đ˛ŅƒĐšŅ‚Đĩ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž, Ņ‰ĐžĐą ĐŋŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ Ņ—Ņ… С ĐžŅĐŊОвĐŊĐžŅ— ҁ҂ҀҖ҇Đēи", + "no_assets_message": "ĐĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ, Ņ‰ĐžĐą виваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ŅĐ˛ĐžŅ” ĐŋĐĩŅ€ŅˆĐĩ Ņ„ĐžŅ‚Đž", + "no_assets_to_show": "НĐĩĐŧĐ°Ņ” ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ Đ´ĐģŅ ĐŋĐžĐēĐ°ĐˇŅƒ", "no_cast_devices_found": "ĐŸŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— Đ´ĐģŅ Ņ‚Ņ€Đ°ĐŊҁĐģŅŅ†Ņ–Ņ— ĐŊĐĩ СĐŊаКдĐĩĐŊĐž", - "no_checksum_local": "КоĐŊŅ‚Ņ€ĐžĐģҌĐŊа ҁ҃Đŧа ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа – ĐŊĐĩĐŧĐžĐļĐģивО ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģи", - "no_checksum_remote": "КоĐŊŅ‚Ņ€ĐžĐģҌĐŊа ҁ҃Đŧа ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа – ĐŊĐĩĐŧĐžĐļĐģивО ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊиК Ņ„Đ°ĐšĐģ", - "no_configuration_needed": "НĐĩ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊа ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–Ņ", + "no_checksum_local": "КоĐŊŅ‚Ņ€ĐžĐģҌĐŊа ҁ҃Đŧа ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа — ĐŊĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", + "no_checksum_remote": "КоĐŊŅ‚Ņ€ĐžĐģҌĐŊа ҁ҃Đŧа ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа — ĐŊĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊиК ĐĩĐģĐĩĐŧĐĩĐŊŅ‚", + "no_configuration_needed": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊĐĩ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐĩ", "no_devices": "НĐĩĐŧĐ°Ņ” Đ°Đ˛Ņ‚ĐžŅ€Đ¸ĐˇĐžĐ˛Đ°ĐŊĐ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—Đ˛", "no_duplicates_found": "Đ”ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛ ĐŊĐĩ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐž.", - "no_exif_info_available": "Đ’Ņ–Đ´ŅŅƒŅ‚ĐŊŅ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž exif", - "no_explore_results_message": "ЗаваĐŊŅ‚Đ°ĐļŅƒĐšŅ‚Đĩ ĐąŅ–ĐģҌ҈Đĩ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš, Ņ‰ĐžĐą ĐŊĐ°ŅĐžĐģОдĐļŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ Đ˛Đ°ŅˆĐžŅŽ ĐēĐžĐģĐĩĐēŅ†Ņ–Ņ”ŅŽ.", - "no_favorites_message": "Đ”ĐžĐ´Đ°Đ˛Đ°ĐšŅ‚Đĩ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž в ĐžĐąŅ€Đ°ĐŊĐĩ, Ņ‰ĐžĐą ŅˆĐ˛Đ¸Đ´ĐēĐž СĐŊĐ°Ņ…ĐžĐ´Đ¸Ņ‚Đ¸ ĐŊаКĐēŅ€Đ°Ņ‰Ņ–", + "no_exif_info_available": "Đ’Ņ–Đ´ŅŅƒŅ‚ĐŊŅ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž Exif", + "no_explore_results_message": "ВиваĐŊŅ‚Đ°ĐļŅƒĐšŅ‚Đĩ ĐąŅ–ĐģҌ҈Đĩ Ņ„ĐžŅ‚Đž, Ņ‰ĐžĐą Đ´ĐžŅĐģŅ–Đ´Đ¸Ņ‚Đ¸ ŅĐ˛ĐžŅŽ ĐēĐžĐģĐĩĐēŅ†Ņ–ŅŽ.", + "no_favorites_message": "Đ”ĐžĐ´Đ°Đ˛Đ°ĐšŅ‚Đĩ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ҃ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐĩ, Ņ‰ĐžĐą ŅˆĐ˛Đ¸Đ´ĐēĐž СĐŊĐ°Ņ…ĐžĐ´Đ¸Ņ‚Đ¸ ĐŊаКĐēŅ€Đ°Ņ‰Ņ–", "no_filters_added": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€Đ¸ ҉Đĩ ĐŊĐĩ дОдаĐŊĐž", - "no_libraries_message": "ĐĄŅ‚Đ˛ĐžŅ€Ņ–Ņ‚ŅŒ СОвĐŊŅ–ŅˆĐŊŅŽ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃ Đ´ĐģŅ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš Ņ– Đ˛Ņ–Đ´ĐĩĐž", - "no_local_assets_found": "З Ņ†Ņ–Ņ”ŅŽ ĐēĐžĐŊŅ‚Ņ€ĐžĐģҌĐŊĐžŅŽ ҁ҃ĐŧĐžŅŽ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐģĐžĐēаĐģҌĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", - "no_location_set": "ĐœŅ–ŅŅ†ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ ĐŊĐĩ Đ˛ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž", - "no_locked_photos_message": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž в ĐžŅĐžĐąĐ¸ŅŅ‚Ņ–Đš ĐŋаĐŋ҆Җ ĐŋŅ€Đ¸Ņ…ĐžĐ˛Đ°ĐŊŅ– Ņ– ĐŊĐĩ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°ŅŽŅ‚ŅŒŅŅ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ Ņ‡Đ¸ ĐŋĐžŅˆŅƒĐē҃ ҃ Đ˛Đ°ŅˆŅ–Đš ĐąŅ–ĐąĐģŅ–ĐžŅ‚Đĩ҆Җ.", + "no_libraries_message": "ĐĄŅ‚Đ˛ĐžŅ€Ņ–Ņ‚ŅŒ СОвĐŊŅ–ŅˆĐŊŅŽ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃ Đ´ĐģŅ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ Ņ„ĐžŅ‚Đž Ņ– Đ˛Ņ–Đ´ĐĩĐž", + "no_local_assets_found": "З Ņ†Ņ–Ņ”ŅŽ ĐēĐžĐŊŅ‚Ņ€ĐžĐģҌĐŊĐžŅŽ ҁ҃ĐŧĐžŅŽ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐģĐžĐēаĐģҌĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", + "no_location_set": "ĐœŅ–ŅŅ†Đĩ ĐŊĐĩ ŅƒŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž", + "no_locked_photos_message": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž в ĐžŅĐžĐąĐ¸ŅŅ‚Ņ–Đš ĐŋаĐŋ҆Җ ĐŋŅ€Đ¸Ņ…ĐžĐ˛Đ°ĐŊŅ– Đš ĐŊĐĩ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°ŅŽŅ‚ŅŒŅŅ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ Ņ‡Đ¸ ĐŋĐžŅˆŅƒĐē҃ ҃ Đ˛Đ°ŅˆŅ–Đš ĐąŅ–ĐąĐģŅ–ĐžŅ‚Đĩ҆Җ.", "no_name": "БĐĩС Ņ–ĐŧĐĩĐŊŅ–", "no_notifications": "НĐĩĐŧĐ°Ņ” ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊҌ", "no_people_found": "Đ›ŅŽĐ´ĐĩĐš, Ņ‰Đž Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°ŅŽŅ‚ŅŒ СаĐŋĐ¸Ņ‚Ņƒ, ĐŊĐĩ СĐŊаКдĐĩĐŊĐž", "no_places": "ĐœŅ–ŅŅ†ŅŒ ĐŊĐĩĐŧĐ°Ņ”", - "no_remote_assets_found": "З Ņ†Ņ–Ņ”ŅŽ ĐēĐžĐŊŅ‚Ņ€ĐžĐģҌĐŊĐžŅŽ ҁ҃ĐŧĐžŅŽ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", + "no_remote_assets_found": "З Ņ†Ņ–Ņ”ŅŽ ĐēĐžĐŊŅ‚Ņ€ĐžĐģҌĐŊĐžŅŽ ҁ҃ĐŧĐžŅŽ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", "no_results": "НĐĩĐŧĐ°Ņ” Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚Ņ–Đ˛", "no_results_description": "ĐĄĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ŅĐ¸ĐŊĐžĐŊŅ–Đŧ айО ĐąŅ–ĐģҌ҈ ĐˇĐ°ĐŗĐ°ĐģҌĐŊĐĩ ĐēĐģŅŽŅ‡ĐžĐ˛Đĩ ҁĐģОвО", - "no_shared_albums_message": "ĐĄŅ‚Đ˛ĐžŅ€Ņ–Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧ, Ņ‰ĐžĐą Đ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–ŅĐŧи Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž С ĐģŅŽĐ´ŅŒĐŧи ҃ Đ˛Đ°ŅˆŅ–Đš ĐŧĐĩŅ€ĐĩĐļŅ–", + "no_shared_albums_message": "ĐĄŅ‚Đ˛ĐžŅ€Ņ–Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧ, Ņ‰ĐžĐą Đ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž С ĐģŅŽĐ´ŅŒĐŧи ҃ Đ˛Đ°ŅˆŅ–Đš ĐŧĐĩŅ€ĐĩĐļŅ–", "no_uploads_in_progress": "НĐĩĐŧĐ°Ņ” аĐēŅ‚Đ¸Đ˛ĐŊĐ¸Ņ… виваĐŊŅ‚Đ°ĐļĐĩĐŊҌ", "none": "ЖодĐĩĐŊ", "not_allowed": "НĐĩ дОСвОĐģĐĩĐŊĐž", @@ -1623,141 +1627,141 @@ "not_selected": "НĐĩ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž", "notes": "ĐĐžŅ‚Đ°Ņ‚Đēи", "nothing_here_yet": "ĐĸŅƒŅ‚ ҉Đĩ ĐŊŅ–Ņ‡ĐžĐŗĐž ĐŊĐĩĐŧĐ°Ņ”", - "notification_permission_dialog_content": "ЊОй ŅƒĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ, ĐŋĐĩŅ€ĐĩĐšĐ´Ņ–Ņ‚ŅŒ Đ´Đž НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ Ņ– ĐŊĐ°Đ´Đ°ĐšŅ‚Đĩ Đ´ĐžĐˇĐ˛Ņ–Đģ.", - "notification_permission_list_tile_content": "ĐĐ°Đ´Đ°Ņ‚Đ¸ Đ´ĐžĐˇĐ˛Ņ–Đģ Đ´ĐģŅ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊҌ.", - "notification_permission_list_tile_enable_button": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐĄĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ", - "notification_permission_list_tile_title": "Đ”ĐžĐˇĐ˛Ņ–Đģ ĐŊа ĐĄĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ", - "notification_toggle_setting_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅŽ ĐŋĐžŅˆŅ‚ĐžŅŽ", + "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", "obtainium_configurator": "КоĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ‚ĐžŅ€ Obtainium", - "obtainium_configurator_instructions": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ Obtainium Đ´ĐģŅ Đ˛ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ Ņ‚Đ° ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ Android ĐąĐĩСĐŋĐžŅĐĩŅ€ĐĩĐ´ĐŊŅŒĐž С Ņ€ĐĩĐģŅ–ĐˇŅƒ Immich ĐŊа GitHub. ĐĄŅ‚Đ˛ĐžŅ€Ņ–Ņ‚ŅŒ ĐēĐģŅŽŅ‡ API Ņ‚Đ° вийĐĩŅ€Ņ–Ņ‚ŅŒ Đ˛Đ°Ņ€Ņ–Đ°ĐŊŅ‚, Ņ‰ĐžĐą ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ ĐŊа ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ Obtainium", + "obtainium_configurator_instructions": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ Obtainium Đ´ĐģŅ ŅƒŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ Ņ‚Đ° ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ Android ĐąĐĩСĐŋĐžŅĐĩŅ€ĐĩĐ´ĐŊŅŒĐž С Ņ€ĐĩĐģŅ–ĐˇŅƒ Immich ĐŊа GitHub. ĐĄŅ‚Đ˛ĐžŅ€Ņ–Ņ‚ŅŒ ĐēĐģŅŽŅ‡ API Ņ‚Đ° вийĐĩŅ€Ņ–Ņ‚ŅŒ Đ˛Đ°Ņ€Ņ–Đ°ĐŊŅ‚, Ņ‰ĐžĐą ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ ĐŊа ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ Obtainium", "ocr": "OCR", "official_immich_resources": "ĐžŅ„Ņ–Ņ†Ņ–ĐšĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸ Immich", "offline": "НĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиК", "offset": "Đ—ŅŅƒĐ˛", "ok": "ОĐē", - "oldest_first": "ĐĄĐŋĐžŅ‡Đ°Ņ‚Đē҃ ĐŊĐ°ĐšŅŅ‚Đ°Ņ€ŅˆŅ–", + "oldest_first": "ĐĄĐŋĐžŅ‡Đ°Ņ‚Đē҃ ĐŊаКдавĐŊŅ–ŅˆŅ–", "on_this_device": "На Ņ†ŅŒĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", - "onboarding": "ВвĐĩĐ´ĐĩĐŊĐŊŅ", + "onboarding": "ĐŸĐžŅ‡Đ°Ņ‚ĐēОвĐĩ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", "onboarding_locale_description": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ йаĐļаĐŊ҃ ĐŧĐžĐ˛Ņƒ. Ви СĐŧĐžĐļĐĩŅ‚Đĩ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ҆Đĩ ĐŋŅ–ĐˇĐŊŅ–ŅˆĐĩ в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ….", - "onboarding_privacy_description": "ĐĐ°ŅŅ‚ŅƒĐŋĐŊŅ– (ĐŊĐĩĐžĐąĐžĐ˛â€™ŅĐˇĐēĐžĐ˛Ņ–) Ņ„ŅƒĐŊĐē҆Җҗ СаĐģĐĩĐļĐ°Ņ‚ŅŒ Đ˛Ņ–Đ´ СОвĐŊŅ–ŅˆĐŊŅ–Ņ… ҁĐĩŅ€Đ˛Ņ–ŅŅ–Đ˛ Ņ– ĐŧĐžĐļŅƒŅ‚ŅŒ ĐąŅƒŅ‚Đ¸ виĐŧĐēĐŊĐĩĐŊŅ– ĐąŅƒĐ´ŅŒ-ĐēĐžĐģи в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ….", + "onboarding_privacy_description": "ĐĐ°ŅŅ‚ŅƒĐŋĐŊŅ– (ĐŊĐĩОйОв'ŅĐˇĐēĐžĐ˛Ņ–) Ņ„ŅƒĐŊĐē҆Җҗ СаĐģĐĩĐļĐ°Ņ‚ŅŒ Đ˛Ņ–Đ´ СОвĐŊŅ–ŅˆĐŊŅ–Ņ… ҁĐģ҃ĐļĐą, Ņ– Ņ—Ņ… ĐŧĐžĐļĐŊа виĐŧĐēĐŊŅƒŅ‚Đ¸ ĐąŅƒĐ´ŅŒ-ĐēĐžĐģи в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ….", "onboarding_server_welcome_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐšĐŧĐž Đ˛Đ°Ņˆ ҁĐĩŅ€Đ˛ĐĩŅ€ С йаСОвиĐŧи ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ°Đŧи.", - "onboarding_theme_description": "ОбĐĩŅ€Ņ–Ņ‚ŅŒ Ņ‚ĐĩĐŧ҃. Ви ĐŧĐžĐļĐĩŅ‚Đĩ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Ņ—Ņ— ĐŋŅ–ĐˇĐŊŅ–ŅˆĐĩ в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ….", + "onboarding_theme_description": "ОбĐĩŅ€Ņ–Ņ‚ŅŒ Ņ‚ĐĩĐŧ҃ ĐžŅ„ĐžŅ€ĐŧĐģĐĩĐŊĐŊŅ Đ´ĐģŅ Đ˛Đ°ŅˆĐžĐŗĐž ҁĐĩŅ€Đ˛ĐĩŅ€Đ°. Ви СĐŧĐžĐļĐĩŅ‚Đĩ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Ņ—Ņ— ĐŋŅ–ĐˇĐŊŅ–ŅˆĐĩ в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ….", "onboarding_user_welcome_description": "ĐŸĐžŅ‡ĐŊĐĩĐŧĐž!", "onboarding_welcome_user": "Đ›Đ°ŅĐēавО ĐŋŅ€ĐžŅĐ¸ĐŧĐž, {user}", "online": "Đ”ĐžŅŅ‚ŅƒĐŋĐŊиК", - "only_favorites": "Đ›Đ¸ŅˆĐĩ ĐžĐąŅ€Đ°ĐŊŅ–", + "only_favorites": "Đ›Đ¸ŅˆĐĩ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐĩ", "open": "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸", "open_calendar": "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸ ĐēаĐģĐĩĐŊĐ´Đ°Ņ€", "open_in_browser": "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸ в ĐąŅ€Đ°ŅƒĐˇĐĩҀҖ", "open_in_map_view": "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸ ĐŊа ĐŧаĐŋŅ–", "open_in_openstreetmap": "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸ в OpenStreetMap", - "open_the_search_filters": "Đ’Ņ–Đ´ĐēŅ€Đ¸ĐšŅ‚Đĩ ҄ҖĐģŅŒŅ‚Ņ€Đ¸ ĐŋĐžŅˆŅƒĐē҃", - "options": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", + "open_the_search_filters": "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸ ҄ҖĐģŅŒŅ‚Ņ€Đ¸ ĐŋĐžŅˆŅƒĐē҃", + "options": "Đ’Đ°Ņ€Ņ–Đ°ĐŊŅ‚Đ¸", "or": "айО", "organize_into_albums": "ĐŖĐŋĐžŅ€ŅĐ´ĐēŅƒĐ˛Đ°Ņ‚Đ¸ в аĐģŅŒĐąĐžĐŧи", - "organize_into_albums_description": "ПоĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐŊĐ°ŅĐ˛ĐŊŅ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— в аĐģŅŒĐąĐžĐŧи, виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅŽŅ‡Đ¸ ĐŋĐžŅ‚ĐžŅ‡ĐŊŅ– ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ—", - "organize_your_library": "ĐžŅ€ĐŗĐ°ĐŊŅ–ĐˇŅƒĐšŅ‚Đĩ ŅĐ˛ĐžŅŽ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", + "organize_into_albums_description": "ПоĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐŊĐ°ŅĐ˛ĐŊŅ– Ņ„ĐžŅ‚Đž в аĐģŅŒĐąĐžĐŧи Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´ĐŊĐž Đ´Đž ĐŋĐžŅ‚ĐžŅ‡ĐŊĐ¸Ņ… ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ—", + "organize_your_library": "ĐŖĐŋĐžŅ€ŅĐ´ĐēŅƒĐšŅ‚Đĩ ŅĐ˛ĐžŅŽ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", "original": "ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ", "other": "ІĐŊ҈Đĩ", "other_devices": "ІĐŊŅˆŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", - "other_entities": "ІĐŊŅˆŅ– Ņ„Đ°ĐšĐģи", + "other_entities": "ІĐŊŅˆŅ– Ой'Ņ”ĐēŅ‚Đ¸", "other_variables": "ІĐŊŅˆŅ– СĐŧŅ–ĐŊĐŊŅ–", "owned": "ВĐģĐ°ŅĐŊŅ–", "owner": "ВĐģĐ°ŅĐŊиĐē", "page": "ĐĄŅ‚ĐžŅ€Ņ–ĐŊĐēа", "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_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_select_partner": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°", + "partner_page_shared_to_title": "Đ”ĐžŅŅ‚ŅƒĐŋ ĐŊадаĐŊĐž", "partner_page_stop_sharing_content": "{partner} ĐąŅ–ĐģҌ҈Đĩ ĐŊĐĩ ĐŧĐ°Ņ‚Đ¸ĐŧĐĩ Đ´ĐžŅŅ‚ŅƒĐŋ҃ Đ´Đž Đ˛Đ°ŅˆĐ¸Ņ… Ņ„ĐžŅ‚Đž.", - "partner_sharing": "ĐĄĐŋŅ–ĐģҌĐŊĐĩ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ", + "partner_sharing": "ĐĄĐŋŅ–ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ Ņ–Đˇ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°Đŧи", "partners": "ĐŸĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ¸", "password": "ĐŸĐ°Ņ€ĐžĐģҌ", "password_does_not_match": "ĐŸĐ°Ņ€ĐžĐģŅ– ĐŊĐĩ ĐˇĐąŅ–ĐŗĐ°ŅŽŅ‚ŅŒŅŅ", "password_required": "ĐŸĐžŅ‚Ņ€Ņ–ĐąĐĩĐŊ ĐŋĐ°Ņ€ĐžĐģҌ", - "password_reset_success": "ĐŸĐ°Ņ€ĐžĐģҌ ĐąŅƒĐģĐž ҃ҁĐŋŅ–ŅˆĐŊĐž ҁĐēиĐŊŅƒŅ‚Đž", + "password_reset_success": "ĐŸĐ°Ņ€ĐžĐģҌ ҁĐēиĐŊŅƒŅ‚Đž", "past_durations": { - "days": "ĐŸŅ€ĐžĐšŅˆĐģĐž {days, plural, one {Đ´ĐĩĐŊҌ} few {# Đ´ĐŊŅ–} many {# Đ´ĐŊŅ–Đ˛} other {# Đ´ĐŊŅ–Đ˛}}", - "hours": "За ĐžŅŅ‚Đ°ĐŊĐŊŅ– {hours, plural, one {ĐŗĐžĐ´Đ¸ĐŊ҃} few {# ĐŗĐžĐ´Đ¸ĐŊи} many {# ĐŗĐžĐ´Đ¸ĐŊ} other {# ĐŗĐžĐ´Đ¸ĐŊи}}", - "years": "ĐŸŅ€ĐžĐšŅˆĐģĐž {years, plural, one {ҀҖĐē} few {# Ņ€ĐžĐēи} many {# Ņ€ĐžĐēŅ–Đ˛} other {# Ņ€ĐžĐē҃}}" + "days": "За {days, plural, one {ĐžŅŅ‚Đ°ĐŊĐŊŅ–Đš # Đ´ĐĩĐŊҌ} few {ĐžŅŅ‚Đ°ĐŊĐŊŅ– # Đ´ĐŊŅ–} many {ĐžŅŅ‚Đ°ĐŊĐŊŅ–Ņ… # Đ´ĐŊŅ–Đ˛} other {ĐžŅŅ‚Đ°ĐŊĐŊŅ–Ņ… # Đ´ĐŊŅ–Đ˛}}", + "hours": "За {hours, plural, one {ĐžŅŅ‚Đ°ĐŊĐŊŅŽ # ĐŗĐžĐ´Đ¸ĐŊ҃} few {ĐžŅŅ‚Đ°ĐŊĐŊŅ– # ĐŗĐžĐ´Đ¸ĐŊи} many {ĐžŅŅ‚Đ°ĐŊĐŊŅ–Ņ… # ĐŗĐžĐ´Đ¸ĐŊ} other {ĐžŅŅ‚Đ°ĐŊĐŊŅ–Ņ… # ĐŗĐžĐ´Đ¸ĐŊ}}", + "years": "За {years, plural, one {ĐžŅŅ‚Đ°ĐŊĐŊŅ–Đš # ҀҖĐē} few {ĐžŅŅ‚Đ°ĐŊĐŊŅ– # Ņ€ĐžĐēи} many {ĐžŅŅ‚Đ°ĐŊĐŊŅ–Ņ… # Ņ€ĐžĐēŅ–Đ˛} other {ĐžŅŅ‚Đ°ĐŊĐŊŅ–Ņ… # Ņ€ĐžĐēŅ–Đ˛}}" }, "path": "ШĐģŅŅ…", "pattern": "ШайĐģĐžĐŊ", - "pause": "ĐŸĐ°ŅƒĐˇĐ°", + "pause": "ĐŸŅ€Đ¸ĐˇŅƒĐŋиĐŊĐ¸Ņ‚Đ¸", "pause_memories": "ĐŸŅ€Đ¸ĐˇŅƒĐŋиĐŊĐ¸Ņ‚Đ¸ ҁĐŋĐžĐŗĐ°Đ´Đ¸", "paused": "ĐŸŅ€Đ¸ĐˇŅƒĐŋиĐŊĐĩĐŊĐž", - "pending": "На Ņ€ĐžĐˇĐŗĐģŅĐ´Ņ–", + "pending": "В ĐžŅ‡Ņ–ĐēŅƒĐ˛Đ°ĐŊĐŊŅ–", "people": "Đ›ŅŽĐ´Đ¸", - "people_edits_count": "Đ’Ņ–Đ´Ņ€ĐĩĐ´Đ°ĐŗĐžĐ˛Đ°ĐŊĐž {count, plural, one {# ĐžŅĐžĐąŅƒ} few {# ĐžŅĐžĐąĐ¸} many {# ĐžŅŅ–Đą} other {# ĐģŅŽĐ´ĐĩĐš}}", - "people_feature_description": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš Ņ– Đ˛Ņ–Đ´ĐĩĐž, ĐˇĐŗŅ€ŅƒĐŋОваĐŊĐ¸Ņ… Са ĐģŅŽĐ´ŅŒĐŧи", - "people_selected": "{count, plural, one {# ĐžĐąŅ€Đ°ĐŊа ĐžŅĐžĐąĐ°} few {# Đ˛Đ¸ĐąŅ€Đ°ĐŊŅ– ĐžŅĐžĐąĐ¸} many {# Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… ĐžŅŅ–Đą} other {# Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… ĐžŅŅ–Đą}}", + "people_edits_count": "Đ’Ņ–Đ´Ņ€ĐĩĐ´Đ°ĐŗĐžĐ˛Đ°ĐŊĐž {count, plural, one {# ĐģŅŽĐ´Đ¸ĐŊ҃} few {# ĐģŅŽĐ´Đ¸ĐŊи} many {# ĐģŅŽĐ´ĐĩĐš} other {# ĐģŅŽĐ´ĐĩĐš}}", + "people_feature_description": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´ Ņ„ĐžŅ‚Đž Ņ– Đ˛Ņ–Đ´ĐĩĐž, ĐˇĐŗŅ€ŅƒĐŋОваĐŊĐ¸Ņ… Са ĐģŅŽĐ´ŅŒĐŧи", + "people_selected": "{count, plural, one {Đ’Đ¸ĐąŅ€Đ°ĐŊĐž # ĐģŅŽĐ´Đ¸ĐŊ҃} few {Đ’Đ¸ĐąŅ€Đ°ĐŊĐž # ĐģŅŽĐ´Đ¸ĐŊи} many {Đ’Đ¸ĐąŅ€Đ°ĐŊĐž # ĐģŅŽĐ´ĐĩĐš} other {Đ’Đ¸ĐąŅ€Đ°ĐŊĐž # ĐģŅŽĐ´ĐĩĐš}}", "people_sidebar_description": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ ĐŊа ĐģŅŽĐ´ĐĩĐš ҃ ĐąŅ–Ņ‡ĐŊŅ–Đš ĐŋаĐŊĐĩĐģŅ–", - "permanent_deletion_warning": "ПоĐŋĐĩŅ€ĐĩĐ´ĐļĐĩĐŊĐŊŅ ĐŋŅ€Đž видаĐģĐĩĐŊĐŊŅ", - "permanent_deletion_warning_setting_description": "ПоĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐļĐĩĐŊĐŊŅ ĐŋŅ€Đ¸ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐžĐŧ҃ видаĐģĐĩĐŊĐŊŅ– Ņ„Đ°ĐšĐģŅ–Đ˛", + "permanent_deletion_warning": "ПоĐŋĐĩŅ€ĐĩĐ´ĐļĐĩĐŊĐŊŅ ĐŋŅ€Đž ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐĩ видаĐģĐĩĐŊĐŊŅ", + "permanent_deletion_warning_setting_description": "ПоĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐļĐĩĐŊĐŊŅ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", "permanently_delete": "ВидаĐģĐ¸Ņ‚Đ¸ ĐŊаСавĐļди", - "permanently_delete_assets_count": "ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", - "permanently_delete_assets_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŊаСавĐļди видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {҆ĐĩĐš Ņ„Đ°ĐšĐģ?} few {҆Җ # Ņ„Đ°ĐšĐģи?} many {҆Җ # Ņ„Đ°ĐšĐģŅ–Đ˛?} other {҆Җ # Ņ„Đ°ĐšĐģŅ–Đ˛?}} ĐĻĐĩ Ņ‚Đ°ĐēĐžĐļ видаĐģĐ¸Ņ‚ŅŒ {count, plural, one {ĐšĐžĐŗĐž С} few {Ņ—Ņ… С} many {Ņ—Ņ… С} other {Ņ—Ņ… С}} аĐģŅŒĐąĐžĐŧ҃(Ņ–Đ˛).", - "permanently_deleted_asset": "ФаКĐģ видаĐģĐĩĐŊĐž ĐŊаСавĐļди", - "permanently_deleted_assets_count": "ВидаĐģĐĩĐŊĐž ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", - "permission": "ДозвоĐģи", - "permission_empty": "ДозвоĐģи ĐŊĐĩ ĐŋОвиĐŊĐŊŅ– ĐąŅƒŅ‚Đ¸ ĐŋĐžŅ€ĐžĐļĐŊŅ–Đŧи", + "permanently_delete_assets_count": "ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "permanently_delete_assets_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŊаСавĐļди видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {҆ĐĩĐš ĐĩĐģĐĩĐŧĐĩĐŊŅ‚? Đ™ĐžĐŗĐž} few {҆Җ # ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸? Đ‡Ņ…} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛? Đ‡Ņ…} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛? Đ‡Ņ…}} Ņ‚Đ°ĐēĐžĐļ ĐąŅƒĐ´Đĩ видаĐģĐĩĐŊĐž С {count, plural, one {ĐšĐžĐŗĐž} few {Ņ—Ņ…ĐŊŅ–Ņ…} many {Ņ—Ņ…ĐŊŅ–Ņ…} other {Ņ—Ņ…ĐŊŅ–Ņ…}} аĐģŅŒĐąĐžĐŧŅ–Đ˛.", + "permanently_deleted_asset": "ЕĐģĐĩĐŧĐĩĐŊŅ‚ видаĐģĐĩĐŊĐž ĐŊаСавĐļди", + "permanently_deleted_assets_count": "ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} 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_denied": "Đ”ĐžŅŅ‚ŅƒĐŋ ĐˇĐ°ĐąĐžŅ€ĐžĐŊĐĩĐŊĐž. ЊОй виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Immich, ĐŊĐ°Đ´Đ°ĐšŅ‚Đĩ дОСвОĐģи Đ´Đž ÂĢĐ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐžÂģ в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ….", "permission_onboarding_permission_granted": "Đ”ĐžŅŅ‚ŅƒĐŋ ĐŊадаĐŊĐž! Đ’ŅĐĩ ĐŗĐžŅ‚ĐžĐ˛Đž.", - "permission_onboarding_permission_limited": "Đ”ĐžŅŅ‚ŅƒĐŋ ОйĐŧĐĩĐļĐĩĐŊĐž. ЊОйи дОСвОĐģĐ¸Ņ‚Đ¸ Immich ŅŅ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— Ņ‚Đ° ĐēĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛ŅŅ–Ņ”ŅŽ ĐŗĐ°ĐģĐĩŅ€ĐĩŅ”ŅŽ, ĐŊĐ°Đ´Đ°ĐšŅ‚Đĩ дОСвОĐģи ĐŊа Ņ„ĐžŅ‚Đž Đš Đ˛Ņ–Đ´ĐĩĐž в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ….", + "permission_onboarding_permission_limited": "Đ”ĐžŅŅ‚ŅƒĐŋ ОйĐŧĐĩĐļĐĩĐŊĐž. ЊОй Đ´Đ°Ņ‚Đ¸ СĐŧĐžĐŗŅƒ Immich ŅŅ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— Ņ‚Đ° ĐēĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛ŅŅ–Ņ”ŅŽ ĐŗĐ°ĐģĐĩŅ€ĐĩŅ”ŅŽ, ĐŊĐ°Đ´Đ°ĐšŅ‚Đĩ дОСвОĐģи ĐŊа Ņ„ĐžŅ‚Đž Đš Đ˛Ņ–Đ´ĐĩĐž в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ….", "permission_onboarding_request": "Đ—Đ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ Immich ĐŋĐžŅ‚Ņ€Ņ–ĐąĐĩĐŊ Đ´ĐžĐˇĐ˛Ņ–Đģ Đ´ĐģŅ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ Đ˛Đ°ŅˆĐ¸Ņ… Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž.", "person": "Đ›ŅŽĐ´Đ¸ĐŊа", - "person_age_months": "{months, plural, one {# ĐŧŅ–ŅŅŅ†ŅŒ} few {# ĐŧŅ–ŅŅŅ†Ņ–} many {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛} other {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛}}", - "person_age_year_months": "1 ҀҖĐē, {months, plural, one {# ĐŧŅ–ŅŅŅ†ŅŒ} few {# ĐŧŅ–ŅŅŅ†Ņ–} many {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛} other {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛}}", - "person_age_years": "{years, plural, one {# ҀҖĐē} few {# Ņ€ĐžĐēи} many {# Ņ€ĐžĐēŅ–Đ˛} other {# Ņ€ĐžĐēŅ–Đ˛}}", + "person_age_months": "Đ’Ņ–Đē: {months, plural, one {# ĐŧŅ–ŅŅŅ†ŅŒ} few {# ĐŧŅ–ŅŅŅ†Ņ–} many {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛} other {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛}}", + "person_age_year_months": "Đ’Ņ–Đē: 1 ҀҖĐē, {months, plural, one {# ĐŧŅ–ŅŅŅ†ŅŒ} few {# ĐŧŅ–ŅŅŅ†Ņ–} many {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛} other {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛}}", + "person_age_years": "Đ’Ņ–Đē: {years, plural, one {# ҀҖĐē} few {# Ņ€ĐžĐēи} many {# Ņ€ĐžĐēŅ–Đ˛} other {# Ņ€ĐžĐēŅ–Đ˛}}", "person_birthdate": "Đ”Đ°Ņ‚Đ° ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ: {date}", "person_hidden": "{name}{hidden, select, true { (ĐŋŅ€Đ¸Ņ…ĐžĐ˛Đ°ĐŊĐž)} other {}}", - "person_recognized": "ĐžŅĐžĐąŅƒ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаĐģи", - "person_selected": "ĐžĐąŅ€Đ°ĐŊа ĐžŅĐžĐąĐ°", - "photo_shared_all_users": "Đ’Đ¸ĐŗĐģŅĐ´Đ°Ņ” Ņ‚Đ°Đē, Ņ‰Đž ви ĐŋĐžĐ´Ņ–ĐģиĐģĐ¸ŅŅ ŅĐ˛ĐžŅ—Đŧи Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–ŅĐŧи С ŅƒŅŅ–Đŧа ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧи айО ҃ Đ˛Đ°Ņ ĐŊĐĩĐŧĐ°Ņ” ĐļОдĐŊĐžĐŗĐž ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°, С ŅĐēиĐŧ ĐŧĐžĐļĐŊа ĐŋĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ.", + "person_recognized": "Đ›ŅŽĐ´Đ¸ĐŊ҃ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаĐŊĐž", + "person_selected": "Đ›ŅŽĐ´Đ¸ĐŊ҃ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž", + "photo_shared_all_users": "ĐĄŅ…ĐžĐļĐĩ, ви вĐļĐĩ ĐŋĐžĐ´Ņ–ĐģиĐģĐ¸ŅŅ Ņ„ĐžŅ‚Đž С ŅƒŅŅ–Đŧа ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧи, айО ĐŊĐĩĐŧĐ°Ņ” С ĐēиĐŧ Đ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ.", "photos": "Đ¤ĐžŅ‚Đž", "photos_and_videos": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", - "photos_count": "{count, plural, one {{count, number} Đ¤ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ} few {{count, number} Đ¤ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—} many {{count, number} Đ¤ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš} other {{count, number} Đ¤ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš}}", - "photos_from_previous_years": "Đ¤ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— ĐŧиĐŊ҃ĐģĐ¸Ņ… Ņ€ĐžĐēŅ–Đ˛ ҃ ҆ĐĩĐš Đ´ĐĩĐŊҌ", - "photos_only": "ĐĸŅ–ĐģҌĐēи Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—", - "pick_a_location": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐŧҖҁ҆Đĩ Ņ€ĐžĐˇŅ‚Đ°ŅˆŅƒĐ˛Đ°ĐŊĐŊŅ", - "pick_custom_range": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ†ŅŒĐēиК Đ´Ņ–Đ°ĐŋаСОĐŊ", - "pick_date_range": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ Đ´Ņ–Đ°ĐŋаСОĐŊ Đ´Đ°Ņ‚", - "pin_code_changed_successfully": "PIN-ĐēОд ҃ҁĐŋŅ–ŅˆĐŊĐž СĐŧŅ–ĐŊĐĩĐŊĐž", - "pin_code_reset_successfully": "PIN-ĐēОд ҃ҁĐŋŅ–ŅˆĐŊĐž ҁĐēиĐŊŅƒŅ‚Đž", - "pin_code_setup_successfully": "PIN-ĐēОд ҃ҁĐŋŅ–ŅˆĐŊĐž ĐŊаĐģĐ°ŅˆŅ‚ĐžĐ˛Đ°ĐŊĐž", + "photos_count": "{count, plural, one {{count, number} Ņ„ĐžŅ‚Đž} few {{count, number} Ņ„ĐžŅ‚Đž} many {{count, number} Ņ„ĐžŅ‚Đž} other {{count, number} Ņ„ĐžŅ‚Đž}}", + "photos_from_previous_years": "Đ¤ĐžŅ‚Đž ĐŧиĐŊ҃ĐģĐ¸Ņ… Ņ€ĐžĐēŅ–Đ˛", + "photos_only": "Đ›Đ¸ŅˆĐĩ Ņ„ĐžŅ‚Đž", + "pick_a_location": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ĐŧҖҁ҆Đĩ", + "pick_custom_range": "Đ”ĐžĐ˛Ņ–ĐģҌĐŊиК Đ´Ņ–Đ°ĐŋаСОĐŊ", + "pick_date_range": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ Đ´Ņ–Đ°ĐŋаСОĐŊ Đ´Đ°Ņ‚", + "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} ĐœŅ–ŅŅ†Đĩ} few {{count, number} ĐœŅ–ŅŅ†Ņ} many {{count, number} ĐœŅ–ŅŅ†ŅŒ} other {{count, number} ĐœŅ–ŅŅ†ŅŒ}}", + "places_count": "{count, plural, one {{count, number} ĐŧҖҁ҆Đĩ} few {{count, number} ĐŧŅ–ŅŅ†Ņ} many {{count, number} ĐŧŅ–ŅŅ†ŅŒ} other {{count, number} ĐŧŅ–ŅŅ†ŅŒ}}", "play": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸", "play_memories": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ҁĐŋĐžĐŗĐ°Đ´Đ¸", - "play_motion_photo": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ Ņ€ŅƒŅ…ĐžĐŧŅ– Ņ„ĐžŅ‚Đž", - "play_or_pause_video": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ айО ĐŋŅ€Đ¸ĐˇŅƒĐŋиĐŊĐĩĐŊĐŊŅ Đ˛Ņ–Đ´ĐĩĐž", - "play_original_video": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐĩ Đ˛Ņ–Đ´ĐĩĐž", - "play_original_video_setting_description": "ĐĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅŽ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐ¸Ņ… Đ˛Ņ–Đ´ĐĩĐž, а ĐŊĐĩ ĐŋĐĩŅ€ĐĩĐēОдОваĐŊĐ¸Ņ…. Đ¯ĐēŅ‰Đž ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊиК Ņ„Đ°ĐšĐģ ĐŊĐĩҁ҃ĐŧҖҁĐŊиК, Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ ĐąŅƒŅ‚Đ¸ ĐŊĐĩĐēĐžŅ€ĐĩĐēŅ‚ĐŊиĐŧ.", - "play_transcoded_video": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐēОдОваĐŊĐĩ Đ˛Ņ–Đ´ĐĩĐž", - "please_auth_to_access": "Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐŋŅ€ĐžĐšĐ´Ņ–Ņ‚ŅŒ Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–ŅŽ", + "play_motion_photo": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Ņ€ŅƒŅ…ĐžĐŧĐĩ Ņ„ĐžŅ‚Đž", + "play_or_pause_video": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ айО ĐŋŅ€Đ¸ĐˇŅƒĐŋиĐŊĐ¸Ņ‚Đ¸ Đ˛Ņ–Đ´ĐĩĐž", + "play_original_video": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐĩ Đ˛Ņ–Đ´ĐĩĐž", + "play_original_video_setting_description": "ĐĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅŽ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐ¸Ņ… Đ˛Ņ–Đ´ĐĩĐž, а ĐŊĐĩ ĐŋĐĩŅ€ĐĩĐēОдОваĐŊĐ¸Ņ…. Đ¯ĐēŅ‰Đž ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊиК ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ ĐŊĐĩҁ҃ĐŧҖҁĐŊиК, Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ ĐąŅƒŅ‚Đ¸ ĐŊĐĩĐēĐžŅ€ĐĩĐēŅ‚ĐŊиĐŧ.", + "play_transcoded_video": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐēОдОваĐŊĐĩ Đ˛Ņ–Đ´ĐĩĐž", + "please_auth_to_access": "ĐĐ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēŅƒĐšŅ‚ĐĩŅŅ Đ´ĐģŅ Đ´ĐžŅŅ‚ŅƒĐŋ҃", "port": "ĐŸĐžŅ€Ņ‚", - "preferences_settings_subtitle": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃", - "preferences_settings_title": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸", + "preferences_settings_subtitle": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ҃ĐŋОдОйаĐŊĐŊŅĐŧи ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃", + "preferences_settings_title": "ĐŖĐŋОдОйаĐŊĐŊŅ", "preparing": "ĐŸŅ–Đ´ĐŗĐžŅ‚ĐžĐ˛Đēа", - "preset": "ПĐĩŅ€ĐĩĐ´Đ˛ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ", + "preset": "ĐŸŅ€ĐĩҁĐĩŅ‚", "preview": "ПоĐŋĐĩŅ€ĐĩĐ´ĐŊŅ–Đš ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´", - "previous": "ПоĐŋĐĩŅ€ĐĩĐ´ĐŊŅ”", + "previous": "ПоĐŋĐĩŅ€ĐĩĐ´ĐŊŅ–Đš", "previous_memory": "ПоĐŋĐĩŅ€ĐĩĐ´ĐŊŅ–Đš ҁĐŋĐžĐŗĐ°Đ´", "previous_or_next_day": "ДĐĩĐŊҌ вĐŋĐĩŅ€ĐĩĐ´/ĐŊаСад", "previous_or_next_month": "ĐœŅ–ŅŅŅ†ŅŒ вĐŋĐĩŅ€ĐĩĐ´/ĐŊаСад", @@ -1769,526 +1773,530 @@ "profile_drawer_app_logs": "Đ–ŅƒŅ€ĐŊаĐģ", "profile_drawer_client_server_up_to_date": "КĐģŅ–Ņ”ĐŊŅ‚ Ņ‚Đ° ҁĐĩŅ€Đ˛ĐĩŅ€ — аĐēŅ‚ŅƒĐ°ĐģҌĐŊŅ–", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "Đ ĐĩĐļиĐŧ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ Đ˛Đ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž. ЊОй Đ˛Đ¸ĐšŅ‚Đ¸, Đ´ĐžĐ˛ĐŗĐž ĐŊĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ СĐŊĐ°Ņ‡ĐžĐē Đ°Đ˛Đ°Ņ‚Đ°Ņ€Đ° ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°.", + "profile_drawer_readonly_mode": "Đ ĐĩĐļиĐŧ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž. ЊОй Đ˛Đ¸ĐšŅ‚Đ¸, ŅƒŅ‚Ņ€Đ¸ĐŧŅƒĐšŅ‚Đĩ СĐŊĐ°Ņ‡ĐžĐē Đ°Đ˛Đ°Ņ‚Đ°Ņ€Đ°.", "profile_image_of_user": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋŅ€ĐžŅ„Ņ–ĐģŅŽ {user}", - "profile_picture_set": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋŅ€ĐžŅ„Ņ–ĐģŅŽ Đ˛ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž.", + "profile_picture_set": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋŅ€ĐžŅ„Ņ–ĐģŅŽ ŅƒŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž.", "public_album": "ĐŸŅƒĐąĐģҖ҇ĐŊиК аĐģŅŒĐąĐžĐŧ", - "public_share": "ĐŸŅƒĐąĐģҖ҇ĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ", - "purchase_account_info": "ĐŸŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐēа", + "public_share": "ĐŸŅƒĐąĐģҖ҇ĐŊиК ҁĐŋŅ–ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ", + "purchase_account_info": "ĐŸŅ€Đ¸Ņ…Đ¸ĐģҌĐŊиĐē", "purchase_activated_subtitle": "Đ”ŅĐēŅƒŅ”ĐŧĐž Са ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐē҃ Immich Ņ‚Đ° ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŊĐžĐŗĐž СайĐĩСĐŋĐĩ҇ĐĩĐŊĐŊŅ С Đ˛Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸Đŧ ĐēОдОĐŧ", "purchase_activated_time": "АĐēŅ‚Đ¸Đ˛ĐžĐ˛Đ°ĐŊĐž {date}", - "purchase_activated_title": "Đ’Đ°Ņˆ ĐēĐģŅŽŅ‡ ĐąŅƒĐģĐž ҃ҁĐŋŅ–ŅˆĐŊĐž аĐēŅ‚Đ¸Đ˛ĐžĐ˛Đ°ĐŊĐž", + "purchase_activated_title": "Đ’Đ°Ņˆ ĐēĐģŅŽŅ‡ аĐēŅ‚Đ¸Đ˛ĐžĐ˛Đ°ĐŊĐž", "purchase_button_activate": "АĐēŅ‚Đ¸Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸", "purchase_button_buy": "ĐšŅƒĐŋĐ¸Ņ‚Đ¸", "purchase_button_buy_immich": "ĐšŅƒĐŋĐ¸Ņ‚Đ¸ Immich", - "purchase_button_never_show_again": "ĐŅ–ĐēĐžĐģи ĐąŅ–ĐģҌ҈Đĩ ĐŊĐĩ ĐŋĐžĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸", + "purchase_button_never_show_again": "Đ‘Ņ–ĐģҌ҈Đĩ ĐŊĐĩ ĐŋĐžĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸", "purchase_button_reminder": "ĐĐ°ĐŗĐ°Đ´Đ°Ņ‚Đ¸ ҇ĐĩŅ€ĐĩС 30 Đ´ĐŊŅ–Đ˛", - "purchase_button_remove_key": "ВидаĐģĐ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡", - "purchase_button_select": "ĐžĐąŅ€Đ°Ņ‚Đ¸", - "purchase_failed_activation": "НĐĩ вдаĐģĐžŅŅ аĐēŅ‚Đ¸Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸! Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ ŅĐ˛ĐžŅŽ ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊ҃ ĐŋĐžŅˆŅ‚Ņƒ Đ´ĐģŅ ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ ĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊĐžĐŗĐž ĐēĐģŅŽŅ‡Đ° ĐŋŅ€ĐžĐ´ŅƒĐēŅ‚Ņƒ!", - "purchase_individual_description_1": "ДĐģŅ Ņ–ĐŊĐ´Đ¸Đ˛Ņ–Đ´ŅƒĐ°ĐģҌĐŊĐžĐŗĐž виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ", - "purchase_individual_description_2": "ĐĄŅ‚Đ°Ņ‚ŅƒŅ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐēи", + "purchase_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_license_subtitle": "ĐšŅƒĐŋŅ–Ņ‚ŅŒ Immich, Ņ‰ĐžĐą ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐŋОдаĐģŅŒŅˆĐ¸Đš Ņ€ĐžĐˇĐ˛Đ¸Ņ‚ĐžĐē ĐŋŅ€ĐžŅ”ĐēŅ‚Ņƒ", + "purchase_lifetime_description": "БĐĩĐˇŅŅ‚Ņ€ĐžĐēОва Đē҃ĐŋŅ–Đ˛ĐģŅ", "purchase_option_title": "ВАРІАНĐĸИ ĐšĐŖĐŸĐ†Đ’Đ›Đ†", - "purchase_panel_info_1": "Đ ĐžĐˇŅ€ĐžĐąĐēа Immich виĐŧĐ°ĐŗĐ°Ņ” ĐąĐ°ĐŗĐ°Ņ‚Đž Ņ‡Đ°ŅŅƒ Ņ‚Đ° ĐˇŅƒŅĐ¸ĐģҌ. Ми ĐŧĐ°Ņ”ĐŧĐž ŅˆŅ‚Đ°Ņ‚ĐŊĐ¸Ņ… Ņ–ĐŊĐļĐĩĐŊĐĩŅ€Ņ–Đ˛, ŅĐēŅ– ĐŋŅ€Đ°Ņ†ŅŽŅŽŅ‚ŅŒ ĐŊад Ņ‚Đ¸Đŧ, Ņ‰ĐžĐą ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸ ĐšĐžĐŗĐž ŅĐēĐžĐŧĐžĐŗĐ° ĐēŅ€Đ°Ņ‰Đ¸Đŧ. ĐĐ°ŅˆĐ° ĐŧŅ–ŅŅ–Ņ — ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸ ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŊĐĩ СайĐĩСĐŋĐĩ҇ĐĩĐŊĐŊŅ С Đ˛Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸Đŧ ĐēОдОĐŧ Ņ‚Đ° ĐĩŅ‚Đ¸Ņ‡ĐŊŅ– ĐąŅ–ĐˇĐŊĐĩҁ-ĐŋŅ€Đ°ĐēŅ‚Đ¸Đēи ŅŅ‚Ņ–ĐšĐēиĐŧ Đ´ĐļĐĩŅ€ĐĩĐģĐžĐŧ Đ´ĐžŅ…ĐžĐ´Ņƒ Đ´ĐģŅ Ņ€ĐžĐˇŅ€ĐžĐąĐŊиĐēŅ–Đ˛ Ņ– ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐĩĐēĐžŅĐ¸ŅŅ‚ĐĩĐŧ҃, Ņ‰Đž ĐŋОваĐļĐ°Ņ” ĐēĐžĐŊŅ„Ņ–Đ´ĐĩĐŊŅ†Ņ–ĐšĐŊŅ–ŅŅ‚ŅŒ, С Ņ€ĐĩаĐģҌĐŊиĐŧи аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛Đ°Đŧи ĐĩĐēҁĐŋĐģŅƒĐ°Ņ‚Đ°Ņ‚ĐžŅ€ŅŅŒĐēиĐŧ Ņ…ĐŧĐ°Ņ€ĐŊиĐŧ ҁĐĩŅ€Đ˛Ņ–ŅĐ°Đŧ.", - "purchase_panel_info_2": "ĐžŅĐēŅ–ĐģҌĐēи Đŧи ĐˇĐžĐąĐžĐ˛â€™ŅĐˇŅƒŅ”ĐŧĐžŅŅ ĐŊĐĩ Đ´ĐžĐ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐģĐ°Ņ‚ĐŊŅ– ОйĐŧĐĩĐļĐĩĐŊĐŊŅ, Ņ†Ņ ĐŋĐžĐē҃ĐŋĐēа ĐŊĐĩ ĐŊĐ°Đ´Đ°ŅŅ‚ŅŒ ваĐŧ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Đ¸Ņ… Ņ„ŅƒĐŊĐēŅ†Ņ–Đš в Immich. Ми ĐŋĐžĐēĐģĐ°Đ´Đ°Ņ”ĐŧĐžŅŅ ĐŊа Ņ‚Đ°ĐēĐ¸Ņ… ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛, ŅĐē ви, Ņ‰ĐžĐą ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋОдаĐģŅŒŅˆĐ¸Đš Ņ€ĐžĐˇĐ˛Đ¸Ņ‚ĐžĐē Immich.", + "purchase_panel_info_1": "Đ ĐžĐˇŅ€ĐžĐąĐēа Immich ĐŋĐžŅ‚Ņ€ĐĩĐąŅƒŅ” ĐąĐ°ĐŗĐ°Ņ‚Đž Ņ‡Đ°ŅŅƒ Ņ‚Đ° ĐˇŅƒŅĐ¸ĐģҌ. Ми ĐŧĐ°Ņ”ĐŧĐž ŅˆŅ‚Đ°Ņ‚ĐŊĐ¸Ņ… Ņ–ĐŊĐļĐĩĐŊĐĩŅ€Ņ–Đ˛, ŅĐēŅ– ĐŋŅ€Đ°Ņ†ŅŽŅŽŅ‚ŅŒ ĐŊад Ņ‚Đ¸Đŧ, Ņ‰ĐžĐą ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸ ĐšĐžĐŗĐž ŅĐēĐžĐŧĐžĐŗĐ° ĐēŅ€Đ°Ņ‰Đ¸Đŧ. ĐĐ°ŅˆĐ° ĐŧŅ–ŅŅ–Ņ — ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸ ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŊĐĩ СайĐĩСĐŋĐĩ҇ĐĩĐŊĐŊŅ С Đ˛Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸Đŧ ĐēОдОĐŧ Ņ‚Đ° ĐĩŅ‚Đ¸Ņ‡ĐŊŅ– ĐąŅ–ĐˇĐŊĐĩҁ-ĐŋŅ€Đ°ĐēŅ‚Đ¸Đēи ŅŅ‚Ņ–ĐšĐēиĐŧ Đ´ĐļĐĩŅ€ĐĩĐģĐžĐŧ Đ´ĐžŅ…ĐžĐ´Ņƒ Đ´ĐģŅ Ņ€ĐžĐˇŅ€ĐžĐąĐŊиĐēŅ–Đ˛ Ņ– ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐĩĐēĐžŅĐ¸ŅŅ‚ĐĩĐŧ҃, Ņ‰Đž ĐŋОваĐļĐ°Ņ” ĐēĐžĐŊŅ„Ņ–Đ´ĐĩĐŊŅ†Ņ–ĐšĐŊŅ–ŅŅ‚ŅŒ, С Ņ€ĐĩаĐģҌĐŊиĐŧи аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛Đ°Đŧи ĐĩĐēҁĐŋĐģŅƒĐ°Ņ‚Đ°Ņ‚ĐžŅ€ŅŅŒĐēиĐŧ Ņ…ĐŧĐ°Ņ€ĐŊиĐŧ ҁĐģ҃ĐļйаĐŧ.", + "purchase_panel_info_2": "ĐžŅĐēŅ–ĐģҌĐēи Đŧи Đ˛ĐˇŅĐģи ĐŊа ҁĐĩĐąĐĩ СОйОв'ŅĐˇĐ°ĐŊĐŊŅ ĐŊĐĩ Đ´ĐžĐ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐģĐ°Ņ‚ĐŊŅ– ОйĐŧĐĩĐļĐĩĐŊĐŊŅ, Ņ†Ņ Đē҃ĐŋŅ–Đ˛ĐģŅ ĐŊĐĩ ĐŊĐ°Đ´Đ°ŅŅ‚ŅŒ ваĐŧ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Đ¸Ņ… Ņ„ŅƒĐŊĐēŅ†Ņ–Đš в Immich. ПодаĐģŅŒŅˆĐ¸Đš Ņ€ĐžĐˇĐ˛Đ¸Ņ‚ĐžĐē Immich СаĐģĐĩĐļĐ¸Ņ‚ŅŒ Đ˛Ņ–Đ´ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐēи Ņ‚Đ°ĐēĐ¸Ņ… ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛, ŅĐē ви.", "purchase_panel_title": "ĐŸŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐŋŅ€ĐžŅ”ĐēŅ‚", "purchase_per_server": "На ҁĐĩŅ€Đ˛ĐĩŅ€", "purchase_per_user": "На ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "purchase_remove_product_key": "ВидаĐģĐ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡ ĐŋŅ€ĐžĐ´ŅƒĐēŅ‚Ņƒ", - "purchase_remove_product_key_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡ ĐŋŅ€ĐžĐ´ŅƒĐēŅ‚Ņƒ?", - "purchase_remove_server_product_key": "ВидаĐģĐ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡ ĐŋŅ€ĐžĐ´ŅƒĐēŅ‚Ņƒ Đ´ĐģŅ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", - "purchase_remove_server_product_key_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡ ĐŋŅ€ĐžĐ´ŅƒĐēŅ‚Ņƒ Đ´ĐģŅ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°?", + "purchase_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_description_2": "ĐĄŅ‚Đ°Ņ‚ŅƒŅ ĐŋŅ€Đ¸Ņ…Đ¸ĐģҌĐŊиĐēа", "purchase_server_title": "ĐĄĐĩŅ€Đ˛ĐĩŅ€", "purchase_settings_server_activated": "КĐģŅŽŅ‡ ĐŋŅ€ĐžĐ´ŅƒĐēŅ‚Ņƒ ҁĐĩŅ€Đ˛ĐĩŅ€Đ° ĐēĐĩŅ€ŅƒŅ”Ņ‚ŅŒŅŅ адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€ĐžĐŧ", - "query_asset_id": "ІдĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ĐžŅ€ Ņ„Đ°ĐšĐģ҃ СаĐŋĐ¸Ņ‚Ņƒ", + "query_asset_id": "ЗаĐŋĐ¸Ņ‚ Са ID ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°", "queue_status": "ĐŖ ҇ĐĩŅ€ĐˇŅ– {count} С {total}", - "rate_asset": "ĐžŅ†Ņ–ĐŊĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ", - "rating": "Đ—ĐžŅ€ŅĐŊиК Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", - "rating_clear": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", - "rating_count": "{count, plural, one {# ĐˇŅ–Ņ€Đēа} few {# ĐˇŅ–Ņ€Đēи} many {# ĐˇŅ–Ņ€ĐžĐē} other {# ĐˇŅ–Ņ€ĐžĐē}}", - "rating_description": "ПоĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ EXIF ĐŊа Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–ĐšĐŊŅ–Đš ĐŋаĐŊĐĩĐģŅ–", - "reaction_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ Ņ€ĐĩаĐē҆Җҗ", - "read_changelog": "ĐŸŅ€ĐžŅ‡Đ¸Ņ‚Đ°Ņ‚Đ¸ СĐŧŅ–ĐŊи в ĐžĐŊОвĐģĐĩĐŊĐŊŅ–", + "rate_asset": "ĐžŅ†Ņ–ĐŊĐ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚", + "rating": "Đ—Ņ–Ņ€ĐēОвиК Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", + "rating_clear": "ĐĄĐēиĐŊŅƒŅ‚Đ¸ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", + "rating_count": "{count, plural, =0 {БĐĩС Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗŅƒ} one {# ĐˇŅ–Ņ€Đēа} few {# ĐˇŅ–Ņ€Đēи} many {# ĐˇŅ–Ņ€ĐžĐē} other {# ĐˇŅ–Ņ€ĐžĐē}}", + "rating_description": "ПоĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ Exif ĐŊа Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–ĐšĐŊŅ–Đš ĐŋаĐŊĐĩĐģŅ–", + "reaction_options": "Đ’Đ°Ņ€Ņ–Đ°ĐŊŅ‚Đ¸ Ņ€ĐĩаĐē҆Җҗ", + "read_changelog": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ĐļŅƒŅ€ĐŊаĐģ СĐŧŅ–ĐŊ", "readonly_mode_disabled": "Đ ĐĩĐļиĐŧ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ виĐŧĐēĐŊĐĩĐŊĐž", - "readonly_mode_enabled": "Đ ĐĩĐļиĐŧ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ Đ˛Đ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž", + "readonly_mode_enabled": "Đ ĐĩĐļиĐŧ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž", "ready_for_upload": "Đ“ĐžŅ‚ĐžĐ˛Đž Đ´Đž виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", "reassign": "ПĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸", - "reassigned_assets_to_existing_person": "ПĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} {name, select, null {ҖҁĐŊŅƒŅŽŅ‡Ņ–Đš ĐžŅĐžĐąŅ–} other {{name}}}", - "reassigned_assets_to_new_person": "ПĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} ĐŊĐžĐ˛Ņ–Đš ĐžŅĐžĐąŅ–", - "reassing_hint": "ĐŸŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐžĐąŅ€Đ°ĐŊŅ– Ņ„Đ°ĐšĐģи ҖҁĐŊŅƒŅŽŅ‡Ņ–Đš ĐžŅĐžĐąŅ–", + "reassigned_assets_to_existing_person": "ПĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} {name, select, null {ĐŊĐ°ŅĐ˛ĐŊŅ–Đš ĐģŅŽĐ´Đ¸ĐŊŅ–} other {{name}}}", + "reassigned_assets_to_new_person": "ПĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} ĐŊĐžĐ˛Ņ–Đš ĐģŅŽĐ´Đ¸ĐŊŅ–", + "reassing_hint": "ĐŸŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ Đ˛Đ¸ĐąŅ€Đ°ĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŊĐ°ŅĐ˛ĐŊŅ–Đš ĐģŅŽĐ´Đ¸ĐŊŅ–", "recent": "НĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊĐž", "recent_albums": "ĐžŅŅ‚Đ°ĐŊĐŊŅ– аĐģŅŒĐąĐžĐŧи", "recent_searches": "НĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊŅ– ĐŋĐžŅˆŅƒĐēĐžĐ˛Ņ– СаĐŋĐ¸Ņ‚Đ¸", "recently_added": "НĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊĐž дОдаĐŊŅ–", - "recently_added_page_title": "НĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊŅ–", - "recently_taken": "НĐĩдавĐŊĐž ĐˇŅ€ĐžĐąĐģĐĩĐŊĐž", - "recently_taken_page_title": "НĐĩдавĐŊĐž ĐˇŅ€ĐžĐąĐģĐĩĐŊŅ–", + "recently_added_page_title": "НĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊĐž дОдаĐŊŅ–", + "recently_taken": "НĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊĐž СĐŊŅŅ‚Ņ–", + "recently_taken_page_title": "НĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊĐž СĐŊŅŅ‚Ņ–", "refresh": "ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸", "refresh_encoded_videos": "ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸ СаĐēОдОваĐŊŅ– Đ˛Ņ–Đ´ĐĩĐž", "refresh_faces": "ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ОйĐģĐ¸Ņ‡Ņ‡Ņ", "refresh_metadata": "ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊŅ–", "refresh_thumbnails": "ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸", - "refreshed": "ОĐŊОвĐģĐĩĐŊиК", - "refreshes_every_file": "ĐŸĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž Ņ‡Đ¸Ņ‚Đ°Ņ” Đ˛ŅŅ– ҖҁĐŊŅƒŅŽŅ‡Ņ– Ņ‚Đ° ĐŊĐžĐ˛Ņ– Ņ„Đ°ĐšĐģи", + "refreshed": "ОĐŊОвĐģĐĩĐŊĐž", + "refreshes_every_file": "ĐŸĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž ĐˇŅ‡Đ¸Ņ‚ŅƒŅ” Đ˛ŅŅ– ĐŊĐ°ŅĐ˛ĐŊŅ– Ņ‚Đ° ĐŊĐžĐ˛Ņ– Ņ„Đ°ĐšĐģи", "refreshing_encoded_video": "ОĐŊОвĐģĐĩĐŊĐŊŅ СаĐēОдОваĐŊĐžĐŗĐž Đ˛Ņ–Đ´ĐĩĐž", "refreshing_faces": "ОĐŊОвĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡", "refreshing_metadata": "ОĐŊОвĐģĐĩĐŊĐŊŅ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ…", "regenerating_thumbnails": "ĐŸĐžĐ˛Ņ‚ĐžŅ€ĐŊĐĩ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€", - "remote": "На ҁĐĩŅ€Đ˛ĐĩҀҖ", - "remote_assets": "Đ’Ņ–Đ´Đ´Đ°ĐģĐĩĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", - "remote_media_summary": "ЗвĐĩĐ´ĐĩĐŊĐŊŅ Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊĐ¸Ņ… ĐŧĐĩĐ´Ņ–Đ°Ņ„Đ°ĐšĐģŅ–Đ˛", + "remote": "Đ’Ņ–Đ´Đ´Đ°ĐģĐĩĐŊиК", + "remote_assets": "Đ’Ņ–Đ´Đ´Đ°ĐģĐĩĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", + "remote_media_summary": "ЗвĐĩĐ´ĐĩĐŊĐŊŅ Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊĐ¸Ņ… ĐŧĐĩĐ´Ņ–Đ°", "remove": "ВиĐģŅƒŅ‡Đ¸Ņ‚Đ¸", - "remove_assets_album_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} С аĐģŅŒĐąĐžĐŧ҃?", - "remove_assets_shared_link_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} С Ņ†ŅŒĐžĐŗĐž ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ?", - "remove_assets_title": "ВидаĐģĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи?", - "remove_custom_date_range": "ВидаĐģĐ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ†ŅŒĐēиК Đ´Ņ–Đ°ĐŋаСОĐŊ Đ´Đ°Ņ‚", - "remove_deleted_assets": "ВидаĐģĐĩĐŊĐŊŅ Đ°Đ˛Ņ‚ĐžĐŊĐžĐŧĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", - "remove_from_album": "ВидаĐģĐ¸Ņ‚Đ¸ С аĐģŅŒĐąĐžĐŧ҃", - "remove_from_album_action_prompt": "{count} видаĐģĐĩĐŊĐž С аĐģŅŒĐąĐžĐŧ҃", - "remove_from_favorites": "ВидаĐģĐ¸Ņ‚Đ¸ С ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž", + "remove_assets_album_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}} С аĐģŅŒĐąĐžĐŧ҃?", + "remove_assets_shared_link_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} 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 {# Ņ„Đ°ĐšĐģ҃} few {# Ņ„Đ°ĐšĐģŅ–Đ˛} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "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, one {ВиĐģŅƒŅ‡ĐĩĐŊĐž # С Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž} few {ВиĐģŅƒŅ‡ĐĩĐŊĐž # С Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž} many {ВиĐģŅƒŅ‡ĐĩĐŊĐž # С Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž} other {ВиĐģŅƒŅ‡ĐĩĐŊĐž # С Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž}}", + "removed_memory": "ВиĐģŅƒŅ‡ĐĩĐŊĐž ҁĐŋĐžĐŗĐ°Đ´", + "removed_photo_from_memory": "ВиĐģŅƒŅ‡ĐĩĐŊĐž Ņ„ĐžŅ‚Đž ĐˇŅ– ҁĐŋĐžĐŗĐ°Đ´Ņƒ", + "removed_tagged_assets": "ВиĐģŅƒŅ‡ĐĩĐŊĐž Ņ‚ĐĩĐŗ Ņ–Đˇ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", "rename": "ПĐĩŅ€ĐĩĐšĐŧĐĩĐŊŅƒĐ˛Đ°Ņ‚Đ¸", - "repair": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ", - "repair_no_results_message": "НĐĩĐ˛Ņ–Đ´ŅŅ‚ĐĩĐļŅƒĐ˛Đ°ĐŊŅ– Ņ‚Đ° Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ– Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊŅ– Ņ‚ŅƒŅ‚", - "replace_with_upload": "ЗаĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŊа виваĐŊŅ‚Đ°ĐļĐĩĐŊĐĩ", + "repair": "ВиĐŋŅ€Đ°Đ˛Đ¸Ņ‚Đ¸", + "repair_no_results_message": "НĐĩĐ˛Ņ–Đ´ŅŅ‚ĐĩĐļŅƒĐ˛Đ°ĐŊŅ– Ņ‚Đ° Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ– Ņ„Đ°ĐšĐģи С'ŅĐ˛ĐģŅŅ‚ŅŒŅŅ Ņ‚ŅƒŅ‚", + "replace_with_upload": "ЗаĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ виваĐŊŅ‚Đ°ĐļĐĩĐŊиĐŧ", "repository": "Đ ĐĩĐŋĐžĐˇĐ¸Ņ‚ĐžŅ€Ņ–Đš", - "require_password": "ВиĐŧĐ°ĐŗĐ°Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", - "require_user_to_change_password_on_first_login": "ВиĐŧĐ°ĐŗĐ°Ņ‚Đ¸ Đ˛Ņ–Đ´ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° СĐŧŅ–ĐŊŅŽĐ˛Đ°Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ ĐŋŅ€Đ¸ ĐŋĐĩŅ€ŅˆĐžĐŧ҃ Đ˛Ņ…ĐžĐ´Ņ–", - "rescan": "ПĐĩŅ€ĐĩҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ", + "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_success": "PIN-ĐēОд ҁĐēиĐŊŅƒŅ‚Đž", "reset_pin_code_with_password": "Ви СавĐļди ĐŧĐžĐļĐĩŅ‚Đĩ ҁĐēиĐŊŅƒŅ‚Đ¸ ŅĐ˛Ņ–Đš PIN-ĐēОд Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ ĐŋĐ°Ņ€ĐžĐģŅ", - "reset_sqlite": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐąĐ°ĐˇŅƒ даĐŊĐ¸Ņ… SQLite", + "reset_sqlite": "ĐĄĐēиĐŊŅƒŅ‚Đ¸ ĐąĐ°ĐˇŅƒ даĐŊĐ¸Ņ… SQLite", "reset_sqlite_clear_app_data": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ даĐŊŅ–", - "reset_sqlite_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ Đ˛ŅŅ– даĐŊŅ–? ĐĻĐĩ видаĐģĐ¸Ņ‚ŅŒ Đ˛ŅŅ– ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ– виКдĐĩ С виКдĐĩ С Đ˛Đ°ŅˆĐžĐŗĐž ОйĐģŅ–ĐēĐžĐ˛ĐžĐŗĐž СаĐŋĐ¸ŅŅƒŅŽ", - "reset_sqlite_confirmation_note": "ĐŖĐ˛Đ°ĐŗĐ°: ВаĐŧ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž ĐąŅƒĐ´Đĩ ĐŋĐĩŅ€ĐĩСаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧ҃ ĐŋҖҁĐģŅ ĐžŅ‡Đ¸ŅŅ‚Đēи.", - "reset_sqlite_done": "ДаĐŊŅ– ĐąŅƒĐģĐž ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐž. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐŋĐĩŅ€ĐĩСаваĐŊŅ‚Đ°ĐļŅ‚Đĩ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧ҃ Ņ– СĐŊĐžĐ˛Ņƒ ŅƒĐ˛Ņ–ĐšĐ´Ņ–Ņ‚ŅŒ ҃ ŅĐ˛Ņ–Đš ОйĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ.", - "reset_sqlite_success": "Đ‘Đ°ĐˇŅƒ даĐŊĐ¸Ņ… SQLite ҃ҁĐŋŅ–ŅˆĐŊĐž ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐž", - "reset_to_default": "ĐĄĐēиĐŊŅƒŅ‚Đ¸ Đ´Đž ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Са СаĐŧĐžĐ˛Ņ‡ŅƒĐ˛Đ°ĐŊĐŊŅĐŧ", - "resolution": "Đ ĐžĐˇĐ´Ņ–ĐģҌĐŊа Đ—Đ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ", + "reset_sqlite_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ даĐŊŅ– ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃? ĐŖŅŅ– ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐąŅƒĐ´Đĩ видаĐģĐĩĐŊĐž, а ҁĐĩаĐŊҁ — СавĐĩŅ€ŅˆĐĩĐŊĐž.", + "reset_sqlite_confirmation_note": "ĐŖĐ˛Đ°ĐŗĐ°: ВаĐŧ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž ĐąŅƒĐ´Đĩ ĐŋĐĩŅ€ĐĩСаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē ĐŋҖҁĐģŅ ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐŊŅ.", + "reset_sqlite_done": "ДаĐŊŅ– ĐąŅƒĐģĐž ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐž. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐŋĐĩŅ€ĐĩСаĐŋŅƒŅŅ‚Ņ–Ņ‚ŅŒ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē Ņ– СĐŊĐžĐ˛Ņƒ ŅƒĐ˛Ņ–ĐšĐ´Ņ–Ņ‚ŅŒ ҃ ŅĐ˛Ņ–Đš ОйĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ.", + "reset_sqlite_success": "Đ‘Đ°ĐˇŅƒ даĐŊĐ¸Ņ… SQLite ҁĐēиĐŊŅƒŅ‚Đž", + "reset_to_default": "ĐĄĐēиĐŊŅƒŅ‚Đ¸ Đ´Đž Ņ‚Đ¸ĐŋĐžĐ˛Đ¸Ņ…", + "resolution": "Đ ĐžĐˇĐ´Ņ–ĐģҌĐŊа ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ", "resolve_duplicates": "ĐŖŅŅƒĐŊŅƒŅ‚Đ¸ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸", "resolved_all_duplicates": "ĐŖŅŅ– Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸ ҃ҁ҃ĐŊŅƒŅ‚Đž", "restore": "Đ’Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸", "restore_all": "Đ’Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Đ˛ŅĐĩ", "restore_trash_action_prompt": "{count} Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž С ĐēĐžŅˆĐ¸Đēа", "restore_user": "Đ’Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "restored_asset": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊиК Ņ„Đ°ĐšĐģ", + "restored_asset": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚", "resume": "ĐŸŅ€ĐžĐ´ĐžĐ˛ĐļĐ¸Ņ‚Đ¸", "resume_paused_jobs": "Đ’Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ {count, plural, one {# ĐŋŅ€Đ¸ĐˇŅƒĐŋиĐŊĐĩĐŊĐĩ СавдаĐŊĐŊŅ} few {# ĐŋŅ€Đ¸ĐˇŅƒĐŋиĐŊĐĩĐŊŅ– СавдаĐŊĐŊŅ} many {# ĐŋŅ€Đ¸ĐˇŅƒĐŋиĐŊĐĩĐŊĐ¸Ņ… СавдаĐŊҌ} other {# ĐŋŅ€Đ¸ĐˇŅƒĐŋиĐŊĐĩĐŊĐ¸Ņ… СавдаĐŊҌ}}", "retry_upload": "ĐŸĐžĐ˛Ņ‚ĐžŅ€Đ¸Ņ‚Đ¸ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", "review_duplicates": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸", - "review_large_files": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´ вĐĩĐģиĐēĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", + "review_large_files": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ вĐĩĐģиĐēŅ– Ņ„Đ°ĐšĐģи", "role": "Đ ĐžĐģҌ", "role_editor": "Đ ĐĩдаĐēŅ‚ĐžŅ€", - "role_viewer": "ГĐģŅĐ´Đ°Ņ‡", - "running": "АĐēŅ‚Đ¸Đ˛ĐŊиК", + "role_viewer": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‡", + "running": "ВиĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ", "save": "ЗбĐĩŅ€ĐĩĐŗŅ‚Đ¸", "save_to_gallery": "ЗбĐĩŅ€ĐĩĐŗŅ‚Đ¸ в ĐŗĐ°ĐģĐĩŅ€ĐĩŅŽ", "saved": "ЗбĐĩŅ€ĐĩĐļĐĩĐŊĐž", - "saved_api_key": "ЗбĐĩŅ€ĐĩĐļĐĩĐŊŅ– ĐēĐģŅŽŅ‡Ņ– API", + "saved_api_key": "КĐģŅŽŅ‡ API СйĐĩŅ€ĐĩĐļĐĩĐŊĐž", "saved_profile": "ĐŸŅ€ĐžŅ„Ņ–ĐģҌ СйĐĩŅ€ĐĩĐļĐĩĐŊĐž", "saved_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ СйĐĩŅ€ĐĩĐļĐĩĐŊĐž", - "say_something": "ĐĄĐēаĐļŅ–Ņ‚ŅŒ Ņ‰Đž-ĐŊĐĩĐąŅƒĐ´ŅŒ", + "say_something": "НаĐŋĐ¸ŅˆŅ–Ņ‚ŅŒ Ņ‰ĐžŅŅŒ", "scaffold_body_error_occurred": "ВиĐŊиĐēĐģа ĐŋĐžĐŧиĐģĐēа", - "scan": "ĐĄĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ", + "scaffold_body_error_unrecoverable": "ВиĐŊиĐēĐģа ĐēŅ€Đ¸Ņ‚Đ¸Ņ‡ĐŊа ĐŋĐžĐŧиĐģĐēа. ĐĐ°Đ´Ņ–ŅˆĐģŅ–Ņ‚ŅŒ ĐžĐŋĐ¸Ņ ĐŋĐžĐŧиĐģĐēи Ņ‚Đ° ҁ҂ĐĩĐē виĐēĐģиĐēŅ–Đ˛ ĐŊа Discord айО GitHub, Ņ‰ĐžĐą Đŧи ĐŧĐžĐŗĐģи Đ´ĐžĐŋĐžĐŧĐžĐŗŅ‚Đ¸. Đ¯ĐēŅ‰Đž ваĐŧ ĐŋĐžŅ€Đ°Đ´Đ¸Đģи, ви ĐŧĐžĐļĐĩŅ‚Đĩ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ даĐŊŅ– ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ ĐŊиĐļ҇Đĩ.", + "scan": "ĐĄĐēаĐŊŅƒĐ˛Đ°Ņ‚Đ¸", "scan_all_libraries": "ĐĄĐēаĐŊŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛ŅŅ– ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", "scan_library": "ĐĄĐēаĐŊŅƒĐ˛Đ°Ņ‚Đ¸", "scan_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ", "scanning": "ĐĄĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ", - "scanning_for_album": "ĐĄĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ аĐģŅŒĐąĐžĐŧ҃...", + "scanning_for_album": "ĐŸĐžŅˆŅƒĐē аĐģŅŒĐąĐžĐŧ҃â€Ļ", "search": "ĐŸĐžŅˆŅƒĐē", - "search_albums": "Đ¨ŅƒĐēĐ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи", + "search_albums": "ĐŸĐžŅˆŅƒĐē аĐģŅŒĐąĐžĐŧŅ–Đ˛", "search_by_context": "ĐŸĐžŅˆŅƒĐē Са ĐēĐžĐŊŅ‚ĐĩĐēŅŅ‚ĐžĐŧ", "search_by_description": "ĐŸĐžŅˆŅƒĐē Са ĐžĐŋĐ¸ŅĐžĐŧ", - "search_by_description_example": "ĐŸĐžŅ…Ņ–Đ´ĐŊиК Đ´ĐĩĐŊҌ ҃ ХаĐŋŅ–", - "search_by_filename": "ĐŸĐžŅˆŅƒĐē Са ĐŊĐ°ĐˇĐ˛ĐžŅŽ айО Ņ€ĐžĐˇŅˆĐ¸Ņ€ĐĩĐŊĐŊŅĐŧ Ņ„Đ°ĐšĐģ҃", + "search_by_description_example": "ĐŸĐžŅ…Ņ–Đ´ ҃ ХаĐŋŅ–", + "search_by_filename": "ĐŸĐžŅˆŅƒĐē Са ĐŊĐ°ĐˇĐ˛ĐžŅŽ Ņ„Đ°ĐšĐģ҃ айО Ņ€ĐžĐˇŅˆĐ¸Ņ€ĐĩĐŊĐŊŅĐŧ", "search_by_filename_example": "НаĐŋŅ€Đ¸ĐēĐģад, IMG_1234.JPG айО PNG", "search_by_ocr": "ĐŸĐžŅˆŅƒĐē Са OCR", "search_by_ocr_example": "Đ›Đ°Ņ‚Ņ‚Đĩ", "search_camera_lens_model": "ĐŸĐžŅˆŅƒĐē ĐŧОдĐĩĐģŅ– ĐžĐąâ€™Ņ”ĐēŅ‚Đ¸Đ˛Đ°â€Ļ", - "search_camera_make": "ĐŸĐžŅˆŅƒĐē Đ˛Đ¸Ņ€ĐžĐąĐŊиĐēа ĐēаĐŧĐĩŅ€Đ¸...", - "search_camera_model": "ĐŸĐžŅˆŅƒĐē ĐŧОдĐĩĐģŅ– ĐēаĐŧĐĩŅ€Đ¸...", - "search_city": "ĐŸĐžŅˆŅƒĐē ĐŧŅ–ŅŅ‚Đ°...", - "search_country": "ĐŸĐžŅˆŅƒĐē ĐēŅ€Đ°Ņ—ĐŊи...", + "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_interval": "{start} — {end}", "search_filter_date_title": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ Đ´Ņ–Đ°ĐŋаСОĐŊ Đ´Đ°Ņ‚", "search_filter_display_option_not_in_album": "НĐĩ в аĐģŅŒĐąĐžĐŧŅ–", - "search_filter_display_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", + "search_filter_display_options": "Đ’Đ°Ņ€Ņ–Đ°ĐŊŅ‚Đ¸ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "search_filter_filename": "ĐŸĐžŅˆŅƒĐē Са ĐŊĐ°ĐˇĐ˛ĐžŅŽ Ņ„Đ°ĐšĐģ҃", - "search_filter_location": "ĐœŅ–ŅŅ†ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", - "search_filter_location_title": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", + "search_filter_location": "ĐœŅ–ŅŅ†Đĩ", + "search_filter_location_title": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐŧҖҁ҆Đĩ", "search_filter_media_type": "ĐĸиĐŋ ĐŧĐĩĐ´Ņ–Đ°", "search_filter_media_type_title": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ Ņ‚Đ¸Đŋ ĐŧĐĩĐ´Ņ–Đ°", "search_filter_ocr": "ĐŸĐžŅˆŅƒĐē Са OCR", "search_filter_people_title": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐģŅŽĐ´ĐĩĐš", - "search_filter_star_rating": "Đ—ĐžŅ€ŅĐŊиК Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", - "search_filter_tags_title": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ Ņ‚ĐĩĐŗĐ¸", - "search_for": "Đ¨ŅƒĐēĐ°Ņ‚Đ¸ Đ´ĐģŅ", - "search_for_existing_person": "ĐŸĐžŅˆŅƒĐē ҖҁĐŊŅƒŅŽŅ‡ĐžŅ— ĐžŅĐžĐąĐ¸", + "search_filter_star_rating": "Đ ĐĩĐšŅ‚Đ¸ĐŊĐŗ ĐˇŅ–Ņ€ĐēаĐŧи", + "search_filter_tags_title": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ Ņ‚ĐĩĐŗĐ¸", + "search_for": "Đ¨ŅƒĐēĐ°Ņ‚Đ¸", + "search_for_existing_person": "ĐŸĐžŅˆŅƒĐē ĐŊĐ°ŅĐ˛ĐŊĐžŅ— ĐģŅŽĐ´Đ¸ĐŊи", "search_no_more_result": "Đ‘Ņ–ĐģҌ҈Đĩ Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚Ņ–Đ˛ ĐŊĐĩĐŧĐ°Ņ”", "search_no_people": "НĐĩĐŧĐ°Ņ” ĐģŅŽĐ´ĐĩĐš", - "search_no_people_named": "НĐĩĐŧĐ°Ņ” ĐžŅŅ–Đą С Ņ–ĐŧĐĩĐŊĐĩĐŧ \"{name}\"", + "search_no_people_named": "НĐĩĐŧĐ°Ņ” ĐģŅŽĐ´ĐĩĐš С Ņ–ĐŧĐĩĐŊĐĩĐŧ ÂĢ{name}Âģ", "search_no_result": "Đ ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚Ņ–Đ˛ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž, ҁĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ Ņ–ĐŊŅˆĐ¸Đš СаĐŋĐ¸Ņ‚ айО ĐēĐžĐŧĐąŅ–ĐŊĐ°Ņ†Ņ–ŅŽ", - "search_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ĐŋĐžŅˆŅƒĐē҃", + "search_options": "Đ’Đ°Ņ€Ņ–Đ°ĐŊŅ‚Đ¸ ĐŋĐžŅˆŅƒĐē҃", "search_page_categories": "ĐšĐ°Ņ‚ĐĩĐŗĐžŅ€Ņ–Ņ—", - "search_page_motion_photos": "Đ–Đ¸Đ˛Ņ– Ņ„ĐžŅ‚Đž", - "search_page_no_objects": "НĐĩĐŧĐ°Ņ” Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ— ĐŋŅ€Đž Ņ„Đ°ĐšĐģи", - "search_page_no_places": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž ĐŧŅ–ŅŅ†Ņ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа", + "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_view_all_button": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ Đ˛ŅĐĩ", "search_page_your_activity": "Đ’Đ°ŅˆŅ– Đ´Ņ–Ņ—", "search_page_your_map": "Đ’Đ°ŅˆĐ° ĐŧаĐŋа", - "search_people": "Đ¨ŅƒĐēĐ°Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš", + "search_people": "ĐŸĐžŅˆŅƒĐē ĐģŅŽĐ´ĐĩĐš", "search_places": "ĐŸĐžŅˆŅƒĐē ĐŧŅ–ŅŅ†ŅŒ", - "search_rating": "ĐŸĐžŅˆŅƒĐē Са Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗĐžĐŧ...", + "search_rating": "ĐŸĐžŅˆŅƒĐē Са Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗĐžĐŧâ€Ļ", "search_result_page_new_search_hint": "Новий ĐŋĐžŅˆŅƒĐē", - "search_settings": "ĐŸĐžŅˆŅƒĐē ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", - "search_state": "ĐŸĐžŅˆŅƒĐē Ņ€ĐĩĐŗŅ–ĐžĐŊ҃...", - "search_suggestion_list_smart_search_hint_1": "Đ ĐžĐˇŅƒĐŧĐŊиК ĐŋĐžŅˆŅƒĐē ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž Са СаĐŧĐžĐ˛Ņ‡ŅƒĐ˛Đ°ĐŊĐŊŅĐŧ, Đ´ĐģŅ ĐŋĐžŅˆŅƒĐē҃ Са ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊиĐŧи виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ ŅĐ¸ĐŊŅ‚Đ°ĐēŅĐ¸Ņ. ", + "search_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐžŅˆŅƒĐē҃", + "search_state": "ĐŸĐžŅˆŅƒĐē Ņ€ĐĩĐŗŅ–ĐžĐŊ҃â€Ļ", + "search_suggestion_list_smart_search_hint_1": "Đ ĐžĐˇŅƒĐŧĐŊиК ĐŋĐžŅˆŅƒĐē ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž Ņ‚Đ¸ĐŋОвО, Đ´ĐģŅ ĐŋĐžŅˆŅƒĐē҃ Са ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊиĐŧи виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ ŅĐ¸ĐŊŅ‚Đ°ĐēŅĐ¸Ņ ", "search_suggestion_list_smart_search_hint_2": "m:Đ˛Đ°Ņˆ-ĐŋĐžŅˆŅƒĐēОвиК-Ņ‚ĐĩŅ€ĐŧŅ–ĐŊ", - "search_tags": "ĐŸĐžŅˆŅƒĐē Ņ‚ĐĩĐŗŅ–Đ˛...", - "search_timezone": "ĐŸĐžŅˆŅƒĐē Ņ‡Đ°ŅĐžĐ˛ĐžĐŗĐž ĐŋĐžŅŅŅƒ...", + "search_tags": "ĐŸĐžŅˆŅƒĐē Ņ‚ĐĩĐŗŅ–Đ˛â€Ļ", + "search_timezone": "ĐŸĐžŅˆŅƒĐē Ņ‡Đ°ŅĐžĐ˛ĐžĐŗĐž ĐŋĐžŅŅŅƒâ€Ļ", "search_type": "ĐĸиĐŋ ĐŋĐžŅˆŅƒĐē҃", "search_your_photos": "ĐŸĐžŅˆŅƒĐē ҁĐĩŅ€ĐĩĐ´ Đ˛Đ°ŅˆĐ¸Ņ… Ņ„ĐžŅ‚Đž", - "searching_locales": "ĐĸŅ€Đ¸Đ˛Đ°Ņ” ĐŋĐžŅˆŅƒĐē ĐŋĐĩŅ€ĐĩĐēĐģĐ°Đ´Ņ–Đ˛...", + "searching_locales": "ĐŸĐžŅˆŅƒĐē ĐģĐžĐēаĐģĐĩĐšâ€Ļ", "second": "ĐĄĐĩĐē҃ĐŊда", "see_all_people": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ Đ˛ŅŅ–Ņ… ĐģŅŽĐ´ĐĩĐš", "select": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸", "select_album": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", - "select_album_cover": "ĐžĐąŅ€Đ°Ņ‚Đ¸ ОйĐēĐģадиĐŊĐē҃ аĐģŅŒĐąĐžĐŧ҃", + "select_album_cover": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ОйĐēĐģадиĐŊĐē҃ аĐģŅŒĐąĐžĐŧ҃", "select_albums": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи", "select_all": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ Đ˛ŅĐĩ", "select_all_duplicates": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ Đ˛ŅŅ– Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸", "select_all_in": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ Đ˛ŅĐĩ в {group}", "select_avatar_color": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ĐēĐžĐģŅ–Ņ€ Đ°Đ˛Đ°Ņ‚Đ°Ņ€Đ°", "select_count": "{count, plural, one {Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ #} few {Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ #} many {Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ #} other {Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ #}}", - "select_cutoff_date": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐēŅ–ĐŊ҆ĐĩĐ˛Ņƒ Đ´Đ°Ņ‚Ņƒ", - "select_face": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ОйĐģĐ¸Ņ‡Ņ‡Ņ", - "select_featured_photo": "ĐžĐąŅ€Đ°Ņ‚Đ¸ ĐžĐąŅ€Đ°ĐŊĐĩ Ņ„ĐžŅ‚Đž", - "select_from_computer": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ С ĐēĐžĐŧĐŋ'ŅŽŅ‚ĐĩŅ€Đ°", - "select_keep_all": "ЗаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ Đ˛ŅĐĩ ĐžĐąŅ€Đ°ĐŊĐĩ", + "select_cutoff_date": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ĐēŅ–ĐŊ҆ĐĩĐ˛Ņƒ Đ´Đ°Ņ‚Ņƒ", + "select_face": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ОйĐģĐ¸Ņ‡Ņ‡Ņ", + "select_featured_photo": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ĐŗĐžĐģОвĐŊĐĩ Ņ„ĐžŅ‚Đž", + "select_from_computer": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ С ĐēĐžĐŧĐŋ'ŅŽŅ‚ĐĩŅ€Đ°", + "select_keep_all": "ЗаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ Đ˛ŅŅ– Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸", "select_library_owner": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ вĐģĐ°ŅĐŊиĐēа ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", - "select_new_face": "ĐžĐąŅ€Đ°Ņ‚Đ¸ ĐŊОвĐĩ ОйĐģĐ¸Ņ‡Ņ‡Ņ", + "select_new_face": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ĐŊОвĐĩ ОйĐģĐ¸Ņ‡Ņ‡Ņ", "select_people": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš", - "select_person": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐžŅĐžĐąŅƒ", - "select_person_to_tag": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐģŅŽĐ´Đ¸ĐŊ҃ Đ´ĐģŅ ĐŋОСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ", + "select_person": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ĐģŅŽĐ´Đ¸ĐŊ҃", + "select_person_to_tag": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ĐģŅŽĐ´Đ¸ĐŊ҃ Đ´ĐģŅ ĐŋОСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ", "select_photos": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž", - "select_trash_all": "ВидаĐģĐ¸Ņ‚Đ¸ Đ˛ŅĐĩ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐĩ", + "select_trash_all": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ Đ˛ŅŅ– Đ´Đž ĐēĐžŅˆĐ¸Đēа", "select_user_for_sharing_page_err_album": "НĐĩ вдаĐģĐžŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", - "selected": "ĐžĐąŅ€Đ°ĐŊĐž", - "selected_count": "{count, plural, one {# ĐžĐąŅ€Đ°ĐŊиК} few {# ĐžĐąŅ€Đ°ĐŊŅ–} many {# ĐžĐąŅ€Đ°ĐŊĐ¸Ņ…} other {# ĐžĐąŅ€Đ°ĐŊĐ¸Ņ…}}", - "selected_gps_coordinates": "Đ’Đ¸ĐąŅ€Đ°ĐŊŅ– ĐēĐžĐžŅ€Đ´Đ¸ĐŊĐ°Ņ‚Đ¸", + "selected": "Đ’Đ¸ĐąŅ€Đ°ĐŊĐž", + "selected_count": "{count, plural, one {# Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž} few {# Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž} many {# Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž} other {# Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž}}", + "selected_gps_coordinates": "Đ’Đ¸ĐąŅ€Đ°ĐŊŅ– GPS-ĐēĐžĐžŅ€Đ´Đ¸ĐŊĐ°Ņ‚Đ¸", "send_message": "ĐĐ°Đ´Ņ–ŅĐģĐ°Ņ‚Đ¸ ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊĐŊŅ", - "send_welcome_email": "ĐĐ°Đ´Ņ–ŅˆĐģŅ–Ņ‚ŅŒ Đ˛Ņ–Ņ‚Đ°ĐģҌĐŊиК ĐģĐ¸ŅŅ‚", - "server_endpoint": "ĐĐ´Ņ€ĐĩŅĐ° ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ", + "send_welcome_email": "ĐĐ°Đ´Ņ–ŅĐģĐ°Ņ‚Đ¸ Đ˛Ņ–Ņ‚Đ°ĐģҌĐŊиК ĐģĐ¸ŅŅ‚", + "server_endpoint": "ĐĐ´Ņ€ĐĩŅĐ° ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "server_info_box_app_version": "ВĐĩŅ€ŅŅ–Ņ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃", "server_info_box_server_url": "URL ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "server_offline": "ĐĄĐĩŅ€Đ˛ĐĩŅ€ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиК", "server_online": "ĐĄĐĩŅ€Đ˛ĐĩŅ€ Đ´ĐžŅŅ‚ŅƒĐŋĐŊиК", "server_privacy": "КоĐŊŅ„Ņ–Đ´ĐĩĐŊŅ†Ņ–ĐšĐŊŅ–ŅŅ‚ŅŒ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", - "server_restarting_description": "ĐĻŅ ŅŅ‚ĐžŅ€Ņ–ĐŊĐēа ĐžĐŊĐžĐ˛Đ¸Ņ‚ŅŒŅŅ ĐŧĐ¸Ņ‚Ņ‚Ņ”Đ˛Đž.", - "server_restarting_title": "ĐĄĐĩŅ€Đ˛ĐĩŅ€ ĐŋĐĩŅ€ĐĩСаваĐŊŅ‚Đ°ĐļŅƒŅ”Ņ‚ŅŒŅŅ", + "server_restarting_description": "ĐĻŅ ŅŅ‚ĐžŅ€Ņ–ĐŊĐēа ĐžĐŊĐžĐ˛Đ¸Ņ‚ŅŒŅŅ Са ĐŧĐ¸Ņ‚ŅŒ.", + "server_restarting_title": "ĐĄĐĩŅ€Đ˛ĐĩŅ€ ĐŋĐĩŅ€ĐĩСаĐŋ҃ҁĐēĐ°Ņ”Ņ‚ŅŒŅŅ", "server_stats": "ĐĄŅ‚Đ°Ņ‚Đ¸ŅŅ‚Đ¸Đēа ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "server_update_available": "ОĐŊОвĐģĐĩĐŊĐŊŅ ҁĐĩŅ€Đ˛ĐĩŅ€Đ° Đ´ĐžŅŅ‚ŅƒĐŋĐŊĐĩ", "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_navigation_title": "ĐĐ°Đ˛Ņ–ĐŗĐ°Ņ†Ņ–Ņ ĐŋĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅŅ…", - "setting_image_viewer_help": "ПовĐŊĐžĐĩĐēŅ€Đ°ĐŊĐŊиК ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‡ ҁĐŋĐžŅ‡Đ°Ņ‚Đē҃ СаваĐŊŅ‚Đ°ĐļŅƒŅ” ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ Đ´ĐģŅ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ в ĐŊĐ¸ĐˇŅŒĐēŅ–Đš Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊŅ–Đš ĐˇĐ´Đ°Ņ‚ĐŊĐžŅŅ‚Ņ–, ĐŋĐžŅ‚Ņ–Đŧ СаваĐŊŅ‚Đ°ĐļŅƒŅ” ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ в СĐŧĐĩĐŊ҈ĐĩĐŊŅ–Đš Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊŅ–Đš ĐˇĐ´Đ°Ņ‚ĐŊĐžŅŅ‚Ņ– Đ˛Ņ–Đ´ĐŊĐžŅĐŊĐž ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ҃ (ŅĐēŅ‰Đž вĐēĐģŅŽŅ‡ĐĩĐŊĐž) Ņ– ĐˇŅ€ĐĩŅˆŅ‚ĐžŅŽ СаваĐŊŅ‚Đ°ĐļŅƒŅ” ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ (ŅĐēŅ‰Đž вĐēĐģŅŽŅ‡ĐĩĐŊĐž).", - "setting_image_viewer_original_subtitle": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Đ´ĐģŅ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ С ĐŋОвĐŊĐžŅŽ Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅŽ ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŽ (вĐĩĐģиĐēĐĩ!). ВиĐŧĐēĐŊŅƒŅ‚Đ¸, Ņ‰ĐžĐą СĐŧĐĩĐŊŅˆĐ¸Ņ‚Đ¸ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ даĐŊĐ¸Ņ… (ŅĐē ҇ĐĩŅ€ĐĩС ĐŧĐĩŅ€ĐĩĐļ҃, Ņ‚Đ°Đē Ņ– ĐŊа ĐēĐĩŅˆŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ).", + "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_navigation_enable_subtitle": "Đ¯ĐēŅ‰Đž ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž, ви ĐŧĐžĐļĐĩŅ‚Đĩ ĐŋĐĩŅ€ĐĩŅ…ĐžĐ´Đ¸Ņ‚Đ¸ Đ´Đž ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž айО ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ, ĐŊĐ°Ņ‚Đ¸ŅĐēĐ°ŅŽŅ‡Đ¸ ĐŊа ĐēŅ€Đ°ĐšĐŊŅŽ ĐģŅ–Đ˛Ņƒ айО ĐŋŅ€Đ°Đ˛Ņƒ Ņ‡Đ˛ĐĩŅ€Ņ‚ŅŒ ĐĩĐēŅ€Đ°ĐŊа.", + "setting_image_navigation_enable_title": "ĐĸĐžŅ€ĐēĐŊŅ–Ņ‚ŅŒŅŅ, Ņ‰ĐžĐą ĐŋĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", + "setting_image_navigation_title": "ĐĐ°Đ˛Ņ–ĐŗĐ°Ņ†Ņ–Ņ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅĐŧи", + "setting_image_viewer_help": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‡ ҁĐŋĐžŅ‡Đ°Ņ‚Đē҃ СаваĐŊŅ‚Đ°ĐļŅƒŅ” ĐŧаĐģ҃ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Ņƒ, ĐŋĐžŅ‚Ņ–Đŧ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅ–Đš ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´ ҁĐĩŅ€ĐĩĐ´ĐŊŅŒĐžŅ— Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅ— ĐˇĐ´Đ°Ņ‚ĐŊĐžŅŅ‚Ņ– (ŅĐēŅ‰Đž ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž) Ņ– ĐˇŅ€ĐĩŅˆŅ‚ĐžŅŽ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ (ŅĐēŅ‰Đž ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž).", + "setting_image_viewer_original_subtitle": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ, Ņ‰ĐžĐą СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐĩ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ С ĐŋОвĐŊĐžŅŽ Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅŽ ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŽ (вĐĩĐģиĐēĐĩ!). ВиĐŧĐēĐŊŅ–Ņ‚ŅŒ, Ņ‰ĐžĐą СĐŧĐĩĐŊŅˆĐ¸Ņ‚Đ¸ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ даĐŊĐ¸Ņ… (ŅĐē ҇ĐĩŅ€ĐĩС ĐŧĐĩŅ€ĐĩĐļ҃, Ņ‚Đ°Đē Ņ– в ĐēĐĩŅˆŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ).", "setting_image_viewer_original_title": "ЗаваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐĩ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", - "setting_image_viewer_preview_subtitle": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Đ´ĐģŅ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ҁĐĩŅ€ĐĩĐ´ĐŊŅŒĐžŅ— Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅ— ĐˇĐ´Đ°Ņ‚ĐŊĐžŅŅ‚Ņ–. ВиĐŧĐēĐŊŅƒŅ‚Đ¸, Ņ‰ĐžĐą СаваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ айО виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ҂ҖĐģҌĐēи ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Ņƒ.", + "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_failures_grace_period": "ĐĄĐŋĐžĐ˛Ņ–Ņ‰Đ°Ņ‚Đ¸ ĐŋŅ€Đž ĐŋĐžĐŧиĐģĐēи Ņ„ĐžĐŊĐžĐ˛ĐžĐŗĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ: {duration}", + "setting_notifications_notify_hours": "{count, plural, one {# ĐŗĐžĐ´Đ¸ĐŊа} few {# ĐŗĐžĐ´Đ¸ĐŊи} many {# ĐŗĐžĐ´Đ¸ĐŊ} other {# ĐŗĐžĐ´Đ¸ĐŊ}}", "setting_notifications_notify_immediately": "ĐŊĐĩĐŗĐ°ĐšĐŊĐž", - "setting_notifications_notify_minutes": "{count} Ņ…Đ˛Đ¸ĐģиĐŊ", + "setting_notifications_notify_minutes": "{count, plural, one {# Ņ…Đ˛Đ¸ĐģиĐŊа} few {# Ņ…Đ˛Đ¸ĐģиĐŊи} many {# Ņ…Đ˛Đ¸ĐģиĐŊ} other {# Ņ…Đ˛Đ¸ĐģиĐŊ}}", "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_notifications_notify_seconds": "{count, plural, one {# ҁĐĩĐē҃ĐŊда} few {# ҁĐĩĐē҃ĐŊди} many {# ҁĐĩĐē҃ĐŊĐ´} other {# ҁĐĩĐē҃ĐŊĐ´}}", + "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_auto_play_subtitle": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž ĐŋĐžŅ‡Đ¸ĐŊĐ°Ņ‚Đ¸ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ Đ˛Ņ–Đ´ĐĩĐž ĐŋŅ–Đ´ Ņ‡Đ°Ņ Ņ—Ņ… Đ˛Ņ–Đ´ĐēŅ€Đ¸Ņ‚Ņ‚Ņ", "setting_video_viewer_auto_play_title": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐĩ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ Đ˛Ņ–Đ´ĐĩĐž", "setting_video_viewer_looping_title": "ĐĻиĐēĐģҖ҇ĐŊĐĩ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ", - "setting_video_viewer_original_video_subtitle": "ĐŸŅ€Đ¸ Ņ‚Ņ€Đ°ĐŊҁĐģŅŅ†Ņ–Ņ— Đ˛Ņ–Đ´ĐĩĐž С ҁĐĩŅ€Đ˛ĐĩŅ€Đ° Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ, ĐŊĐ°Đ˛Ņ–Ņ‚ŅŒ ŅĐēŅ‰Đž Đ´ĐžŅŅ‚ŅƒĐŋĐŊа Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. МоĐļĐĩ ĐŋŅ€Đ¸ĐˇĐ˛ĐĩŅŅ‚Đ¸ Đ´Đž ĐąŅƒŅ„ĐĩŅ€Đ¸ĐˇĐ°Ņ†Ņ–Ņ—. Đ’Ņ–Đ´ĐĩĐž, Đ´ĐžŅŅ‚ŅƒĐŋĐŊŅ– ĐģĐžĐēаĐģҌĐŊĐž, Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽŅŽŅ‚ŅŒŅŅ в ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊŅ–Đš ŅĐēĐžŅŅ‚Ņ–, ĐŊĐĩСваĐļĐ°ŅŽŅ‡Đ¸ ĐŊа ҆Đĩ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ.", + "setting_video_viewer_original_video_subtitle": "ĐŸŅ–Đ´ Ņ‡Đ°Ņ ĐŋĐžŅ‚ĐžĐēĐžĐ˛ĐžĐŗĐž Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ Đ˛Ņ–Đ´ĐĩĐž Ņ–Đˇ ҁĐĩŅ€Đ˛ĐĩŅ€Đ° Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ, ĐŊĐ°Đ˛Ņ–Ņ‚ŅŒ ŅĐēŅ‰Đž Đ´ĐžŅŅ‚ŅƒĐŋĐŊĐĩ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. МоĐļĐĩ ĐŋŅ€Đ¸ĐˇĐ˛ĐĩŅŅ‚Đ¸ Đ´Đž ĐąŅƒŅ„ĐĩŅ€Đ¸ĐˇĐ°Ņ†Ņ–Ņ—. Đ’Ņ–Đ´ĐĩĐž, Đ´ĐžŅŅ‚ŅƒĐŋĐŊŅ– ĐģĐžĐēаĐģҌĐŊĐž, Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽŅŽŅ‚ŅŒŅŅ в ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊŅ–Đš ŅĐēĐžŅŅ‚Ņ–, ĐŊĐĩСаĐģĐĩĐļĐŊĐž Đ˛Ņ–Đ´ Ņ†ŅŒĐžĐŗĐž ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ.", "setting_video_viewer_original_video_title": "ĐŸŅ€Đ¸ĐŧŅƒŅĐžĐ˛Đž Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐĩ Đ˛Ņ–Đ´ĐĩĐž", "settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", - "settings_require_restart": "ПĐĩŅ€ĐĩСаваĐŊŅ‚Đ°ĐļŅ‚Đĩ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē Đ´ĐģŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°ĐŊĐŊŅ Ņ†ŅŒĐžĐŗĐž ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", - "settings_saved": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ СйĐĩŅ€ĐĩĐļĐĩĐŊŅ–", + "settings_require_restart": "ПĐĩŅ€ĐĩСаĐŋŅƒŅŅ‚Ņ–Ņ‚ŅŒ Immich, Ņ‰ĐžĐą ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ҆Đĩ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", + "settings_saved": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ СйĐĩŅ€ĐĩĐļĐĩĐŊĐž", "setup_pin_code": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°Ņ‚Đ¸ PIN-ĐēОд", - "share": "ĐŸĐžŅˆĐ¸Ņ€Đ¸Ņ‚Đ¸", - "share_action_prompt": "{count} Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ĐŊĐ°Đ´Ņ–ŅĐģаĐŊĐž", + "share": "ĐŸĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ", + "share_action_prompt": "НадаĐŊĐž ҁĐŋŅ–ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ Đ´Đž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", "share_add_photos": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž", - "share_assets_selected": "{count} ĐžĐąŅ€Đ°ĐŊĐž", - "share_dialog_preparing": "ĐŸŅ–Đ´ĐŗĐžŅ‚ĐžĐ˛Đēа...", + "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_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_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_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_days": "{count, plural, one {# Đ´ĐĩĐŊҌ} few {# Đ´ĐŊŅ–} many {# Đ´ĐŊŅ–Đ˛} other {# Đ´ĐŊŅ–Đ˛}}", "shared_link_edit_expire_after_option_hour": "1 ĐŗĐžĐ´Đ¸ĐŊ҃", - "shared_link_edit_expire_after_option_hours": "{count} ĐŗĐžĐ´Đ¸ĐŊ", + "shared_link_edit_expire_after_option_hours": "{count, plural, one {# ĐŗĐžĐ´Đ¸ĐŊ҃} few {# ĐŗĐžĐ´Đ¸ĐŊи} many {# ĐŗĐžĐ´Đ¸ĐŊ} other {# ĐŗĐžĐ´Đ¸ĐŊ}}", "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_expire_after_option_minutes": "{count, plural, one {# Ņ…Đ˛Đ¸ĐģиĐŊ҃} few {# Ņ…Đ˛Đ¸ĐģиĐŊи} many {# Ņ…Đ˛Đ¸ĐģиĐŊ} other {# Ņ…Đ˛Đ¸ĐģиĐŊ}}", + "shared_link_edit_expire_after_option_months": "{count, plural, one {# ĐŧŅ–ŅŅŅ†ŅŒ} few {# ĐŧŅ–ŅŅŅ†Ņ–} many {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛} other {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛}}", + "shared_link_edit_expire_after_option_year": "{count, plural, one {# ҀҖĐē} few {# Ņ€ĐžĐēи} many {# Ņ€ĐžĐēŅ–Đ˛} other {# Ņ€ĐžĐēŅ–Đ˛}}", "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_error_server_url_fetch": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ URL ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", + "shared_link_expires_day": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС {count, plural, one {# Đ´ĐĩĐŊҌ} few {# Đ´ĐŊŅ–} many {# Đ´ĐŊŅ–Đ˛} other {# Đ´ĐŊŅ–Đ˛}}", + "shared_link_expires_days": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС {count, plural, one {# Đ´ĐĩĐŊҌ} few {# Đ´ĐŊŅ–} many {# Đ´ĐŊŅ–Đ˛} other {# Đ´ĐŊŅ–Đ˛}}", + "shared_link_expires_hour": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС {count, plural, one {# ĐŗĐžĐ´Đ¸ĐŊ҃} few {# ĐŗĐžĐ´Đ¸ĐŊи} many {# ĐŗĐžĐ´Đ¸ĐŊ} other {# ĐŗĐžĐ´Đ¸ĐŊ}}", + "shared_link_expires_hours": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС {count, plural, one {# ĐŗĐžĐ´Đ¸ĐŊ҃} few {# ĐŗĐžĐ´Đ¸ĐŊи} many {# ĐŗĐžĐ´Đ¸ĐŊ} other {# ĐŗĐžĐ´Đ¸ĐŊ}}", + "shared_link_expires_minute": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС {count, plural, one {# Ņ…Đ˛Đ¸ĐģиĐŊ҃} few {# Ņ…Đ˛Đ¸ĐģиĐŊи} many {# Ņ…Đ˛Đ¸ĐģиĐŊ} other {# Ņ…Đ˛Đ¸ĐģиĐŊ}}", + "shared_link_expires_minutes": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС {count, plural, one {# Ņ…Đ˛Đ¸ĐģиĐŊ҃} few {# Ņ…Đ˛Đ¸ĐģиĐŊи} many {# Ņ…Đ˛Đ¸ĐģиĐŊ} other {# Ņ…Đ˛Đ¸ĐģиĐŊ}}", "shared_link_expires_never": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ∞", - "shared_link_expires_second": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС {count} ҁĐĩĐē҃ĐŊĐ´Ņƒ", - "shared_link_expires_seconds": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС {count} ҁĐĩĐē҃ĐŊĐ´", + "shared_link_expires_second": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС {count, plural, one {# ҁĐĩĐē҃ĐŊĐ´Ņƒ} few {# ҁĐĩĐē҃ĐŊди} many {# ҁĐĩĐē҃ĐŊĐ´} other {# ҁĐĩĐē҃ĐŊĐ´}}", + "shared_link_expires_seconds": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС {count, plural, one {# ҁĐĩĐē҃ĐŊĐ´Ņƒ} few {# ҁĐĩĐē҃ĐŊди} many {# ҁĐĩĐē҃ĐŊĐ´} other {# ҁĐĩĐē҃ĐŊĐ´}}", "shared_link_individual_shared": "ІĐŊĐ´Đ¸Đ˛Ņ–Đ´ŅƒĐ°ĐģҌĐŊиК ҁĐŋŅ–ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ", - "shared_link_info_chip_metadata": "EXIF", + "shared_link_info_chip_metadata": "Exif", "shared_link_manage_links": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐŋŅ–ĐģҌĐŊиĐŧи ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧи", - "shared_link_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ҁĐŋŅ–ĐģҌĐŊĐ¸Ņ… ĐŋĐžŅĐ¸ĐģаĐŊҌ", - "shared_link_password_description": "ВиĐŧĐ°ĐŗĐ°Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ Đ´ĐģŅ Đ´ĐžŅŅ‚ŅƒĐŋ҃ Đ´Đž Ņ†ŅŒĐžĐŗĐž ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", + "shared_link_options": "Đ’Đ°Ņ€Ņ–Đ°ĐŊŅ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐ¸Ņ… ĐŋĐžŅĐ¸ĐģаĐŊҌ", + "shared_link_password_description": "ЗаĐŋĐ¸Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ, Ņ‰ĐžĐą ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Đ´ĐžŅŅ‚ŅƒĐŋ Đ´Đž Ņ†ŅŒĐžĐŗĐž ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "shared_links": "ĐĄĐŋŅ–ĐģҌĐŊŅ– ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "shared_links_description": "Đ”Ņ–ĐģŅ–Ņ‚ŅŒŅŅ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž Са ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧ", - "shared_photos_and_videos_count": "{assetCount, plural, other {# ҁĐŋŅ–ĐģҌĐŊŅ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž.}}", - "shared_with_me": "Đ”ĐžŅŅ‚ŅƒĐŋĐŊŅ– ĐŧĐĩĐŊŅ–", + "shared_photos_and_videos_count": "{assetCount, plural, one {# ҁĐŋŅ–ĐģҌĐŊĐĩ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž} few {# ҁĐŋŅ–ĐģҌĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž} many {# ҁĐŋŅ–ĐģҌĐŊĐ¸Ņ… Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž} other {# ҁĐŋŅ–ĐģҌĐŊĐ¸Ņ… Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž}}", + "shared_with_me": "ĐĄĐŋŅ–ĐģҌĐŊŅ– ĐˇŅ– ĐŧĐŊĐžŅŽ", "shared_with_partner": "ĐĄĐŋŅ–ĐģҌĐŊĐž С {partner}", - "sharing": "ĐĄĐŋŅ–ĐģҌĐŊŅ–", - "sharing_enter_password": "Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ввĐĩĐ´Ņ–Ņ‚ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ Đ´ĐģŅ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ ҆ҖҔҗ ŅŅ‚ĐžŅ€Ņ–ĐŊĐēи.", + "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": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ аĐģŅŒĐąĐžĐŧ҃", + "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_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_person_options": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ Đ˛Đ°Ņ€Ņ–Đ°ĐŊŅ‚Đ¸ ĐģŅŽĐ´Đ¸ĐŊи", + "show_progress_bar": "ПоĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ Ņ–ĐŊдиĐēĐ°Ņ‚ĐžŅ€ ĐŋĐžŅŅ‚ŅƒĐŋ҃", "show_schema": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ҁ҅ĐĩĐŧ҃", - "show_search_options": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ĐŋĐžŅˆŅƒĐē҃", + "show_search_options": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ Đ˛Đ°Ņ€Ņ–Đ°ĐŊŅ‚Đ¸ ĐŋĐžŅˆŅƒĐē҃", "show_shared_links": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊŅ– ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", - "show_slideshow_transition": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩŅ…Ņ–Đ´ ҁĐģаКд-ŅˆĐžŅƒ", - "show_supporter_badge": "ЗĐŊĐ°Ņ‡ĐžĐē ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐēи", - "show_supporter_badge_description": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ СĐŊĐ°Ņ‡ĐžĐē ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐēи", + "show_slideshow_transition": "ПоĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩŅ…Ņ–Đ´ ҁĐģаКд-ŅˆĐžŅƒ", + "show_supporter_badge": "ЗĐŊĐ°Ņ‡ĐžĐē ĐŋŅ€Đ¸Ņ…Đ¸ĐģҌĐŊиĐēа", + "show_supporter_badge_description": "ПоĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ СĐŊĐ°Ņ‡ĐžĐē ĐŋŅ€Đ¸Ņ…Đ¸ĐģҌĐŊиĐēа", "show_text_recognition": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ Ņ‚ĐĩĐēŅŅ‚Ņƒ", "show_text_search_menu": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ĐŧĐĩĐŊŅŽ Ņ‚ĐĩĐēŅŅ‚ĐžĐ˛ĐžĐŗĐž ĐŋĐžŅˆŅƒĐē҃", "shuffle": "ПĐĩŅ€ĐĩĐŧŅ–ŅˆĐ°Ņ‚Đ¸", "sidebar": "Đ‘Ņ–Ņ‡ĐŊа ĐŋаĐŊĐĩĐģҌ", - "sidebar_display_description": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐˇĐ¸Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ ĐŊа ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´ ҃ ĐąŅ–Ņ‡ĐŊŅ–Đš ĐŋаĐŊĐĩĐģŅ–", - "sign_out": "Đ’Đ¸Ņ…Ņ–Đ´", + "sidebar_display_description": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ ĐŊа ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´ ҃ ĐąŅ–Ņ‡ĐŊŅ–Đš ĐŋаĐŊĐĩĐģŅ–", + "sign_out": "Đ’Đ¸ĐšŅ‚Đ¸", "sign_up": "Đ—Đ°Ņ€ĐĩŅ”ŅŅ‚Ņ€ŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ", "size": "РОСĐŧŅ–Ņ€", "skip_to_content": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž вĐŧŅ–ŅŅ‚Ņƒ", "skip_to_folders": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŋаĐŋĐžĐē", "skip_to_tags": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž Ņ‚ĐĩĐŗŅ–Đ˛", - "slideshow": "ĐĄĐģĐ°ĐšĐ´ŅˆĐžŅƒ", - "slideshow_repeat": "ĐŸĐžĐ˛Ņ‚ĐžŅ€Đ¸Ņ‚Đ¸ ҁĐģаКд-ŅˆĐžŅƒ", + "slideshow": "ĐĄĐģаКд-ŅˆĐžŅƒ", + "slideshow_repeat": "ĐŸĐžĐ˛Ņ‚ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ҁĐģаКд-ŅˆĐžŅƒ", "slideshow_repeat_description": "ПовĐĩŅ€ĐŊĐĩĐŊĐŊŅ Đ´Đž ĐŋĐžŅ‡Đ°Ņ‚Đē҃ ĐŋҖҁĐģŅ СавĐĩŅ€ŅˆĐĩĐŊĐŊŅ ҁĐģаКд-ŅˆĐžŅƒ", "slideshow_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐģаКд-ŅˆĐžŅƒ", - "sort_albums_by": "ĐĄĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи Са...", + "sort_albums_by": "ĐĄĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи Саâ€Ļ", "sort_created": "Đ”Đ°Ņ‚Đ° ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ", - "sort_items": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ Ņ„Đ°ĐšĐģŅ–Đ˛", + "sort_items": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", "sort_modified": "Đ”Đ°Ņ‚Đ° СĐŧŅ–ĐŊи", "sort_newest": "НайĐŊĐžĐ˛Ņ–ŅˆĐĩ Ņ„ĐžŅ‚Đž", - "sort_oldest": "ĐĄŅ‚Đ°Ņ€Ņ– Ņ„ĐžŅ‚Đž", + "sort_oldest": "ĐĐ°ĐšŅŅ‚Đ°Ņ€Ņ–ŅˆĐĩ Ņ„ĐžŅ‚Đž", "sort_people_by_similarity": "ĐĄĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš Са ŅŅ…ĐžĐļŅ–ŅŅ‚ŅŽ", - "sort_recent": "НĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊŅ–", - "sort_title": "Đ—Đ°ĐŗĐžĐģОвОĐē", + "sort_recent": "ĐĐ°ĐšŅĐ˛Ņ–ĐļŅ–ŅˆĐĩ Ņ„ĐžŅ‚Đž", + "sort_title": "Назва", "source": "ДĐļĐĩŅ€ĐĩĐģĐž", - "stack": "ĐŖ ŅŅ‚ĐžĐŋĐē҃", - "stack_action_prompt": "Đ—ĐŗŅ€ŅƒĐŋОваĐŊĐž: {count}", + "stack": "Đ—ĐŗŅ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸", + "stack_action_prompt": "{count} ĐˇĐŗŅ€ŅƒĐŋОваĐŊĐž", "stack_duplicates": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸", "stack_select_one_photo": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ОдĐŊĐĩ ĐžŅĐŊОвĐŊĐĩ Ņ„ĐžŅ‚Đž Đ´ĐģŅ ĐŗŅ€ŅƒĐŋи", - "stack_selected_photos": "Đ—ĐŗŅ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ ĐžĐąŅ€Đ°ĐŊŅ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—", - "stacked_assets_count": "Đ—ĐŗŅ€ŅƒĐŋОваĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "stack_selected_photos": "Đ—ĐŗŅ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛Đ¸ĐąŅ€Đ°ĐŊŅ– Ņ„ĐžŅ‚Đž", + "stacked_assets_count": "Đ—ĐŗŅ€ŅƒĐŋОваĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", "stacktrace": "ĐĄŅ‚ĐĩĐē виĐēĐģиĐēŅ–Đ˛", - "start": "ĐĄŅ‚Đ°Ņ€Ņ‚", + "start": "ĐŸĐžŅ‡Đ°Ņ‚Đ¸", "start_date": "Đ”Đ°Ņ‚Đ° ĐŋĐžŅ‡Đ°Ņ‚Đē҃", "start_date_before_end_date": "Đ”Đ°Ņ‚Đ° ĐŋĐžŅ‡Đ°Ņ‚Đē҃ ĐŧĐ°Ņ” ĐąŅƒŅ‚Đ¸ Ņ€Đ°ĐŊŅ–ŅˆĐĩ Đ´Đ°Ņ‚Đ¸ СавĐĩŅ€ŅˆĐĩĐŊĐŊŅ", "state": "Đ ĐĩĐŗŅ–ĐžĐŊ", "status": "ĐĄŅ‚Đ°ĐŊ", "stop_casting": "Đ—ŅƒĐŋиĐŊĐ¸Ņ‚Đ¸ Ņ‚Ņ€Đ°ĐŊҁĐģŅŅ†Ņ–ŅŽ", - "stop_motion_photo": "Đ¤ĐžŅ‚Đž \"ĐĄŅ‚ĐžĐŋ-ĐŧĐžŅƒŅˆĐĩĐŊ\"", - "stop_photo_sharing": "Đ—ŅƒĐŋиĐŊĐ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ Đ´Đž Đ˛Đ°ŅˆĐ¸Ņ… Ņ„ĐžŅ‚Đž?", - "stop_photo_sharing_description": "{partner} ĐąŅ–ĐģҌ҈Đĩ ĐŊĐĩ ĐŧĐ°Ņ‚Đ¸ĐŧĐĩ Đ´ĐžŅŅ‚ŅƒĐŋ҃ Đ´Đž Đ˛Đ°ŅˆĐ¸Ņ… Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš.", - "stop_sharing_photos_with_user": "ĐŸŅ€Đ¸ĐŋиĐŊĐ¸Ņ‚Đ¸ Đ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ ŅĐ˛ĐžŅ—Đŧи Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–ŅĐŧи С Ņ†Đ¸Đŧ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ĐĩĐŧ", + "stop_motion_photo": "Đ—ŅƒĐŋиĐŊĐ¸Ņ‚Đ¸ Ņ€ŅƒŅ…ĐžĐŧĐĩ Ņ„ĐžŅ‚Đž", + "stop_photo_sharing": "ĐŸŅ€Đ¸ĐŋиĐŊĐ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ Đ´Đž Đ˛Đ°ŅˆĐ¸Ņ… Ņ„ĐžŅ‚Đž?", + "stop_photo_sharing_description": "{partner} ĐąŅ–ĐģҌ҈Đĩ ĐŊĐĩ ĐŧĐ°Ņ‚Đ¸ĐŧĐĩ Đ´ĐžŅŅ‚ŅƒĐŋ҃ Đ´Đž Đ˛Đ°ŅˆĐ¸Ņ… Ņ„ĐžŅ‚Đž.", + "stop_sharing_photos_with_user": "ĐŸŅ€Đ¸ĐŋиĐŊĐ¸Ņ‚Đ¸ Đ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ ŅĐ˛ĐžŅ—Đŧи Ņ„ĐžŅ‚Đž С Ņ†Đ¸Đŧ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ĐĩĐŧ", "storage": "ĐĄŅ…ĐžĐ˛Đ¸Ņ‰Đĩ", - "storage_label": "ĐœŅ–Ņ‚Đēа Đ´ĐģŅ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ", - "storage_quota": "ĐžĐąŅŅĐŗ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", + "storage_label": "ĐœŅ–Ņ‚Đēа ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", + "storage_quota": "ĐšĐ˛ĐžŅ‚Đ° ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", "storage_usage": "{used} С {available} виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐž", - "submit": "ĐŸŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´Đ¸Ņ‚Đ¸", - "success": "ĐŖŅĐŋŅ–ŅˆĐŊĐž", + "submit": "ĐĐ°Đ´Ņ–ŅĐģĐ°Ņ‚Đ¸", + "success": "Đ“ĐžŅ‚ĐžĐ˛Đž", "suggestions": "ĐŸŅ€ĐžĐŋĐžĐˇĐ¸Ņ†Ņ–Ņ—", "sunrise_on_the_beach": "ĐĄĐ˛Ņ–Ņ‚Đ°ĐŊĐžĐē ĐŊа ĐŋĐģŅĐļŅ–", "support": "ĐŸŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐēа", "support_and_feedback": "ĐŸŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐēа Ņ‚Đ° ĐˇĐ˛ĐžŅ€ĐžŅ‚ĐŊиК Св'ŅĐˇĐžĐē", - "support_third_party_description": "Đ’Đ°ŅˆŅƒ ŅƒŅŅ‚Đ°ĐŊОвĐē҃ Immich ĐąŅƒĐģĐž ҃ĐŋаĐēОваĐŊĐž ҂ҀĐĩŅ‚ŅŒĐžŅŽ ŅŅ‚ĐžŅ€ĐžĐŊĐžŅŽ. ĐŸŅ€ĐžĐąĐģĐĩĐŧи, С ŅĐēиĐŧи ви ŅŅ‚Đ¸ĐēĐ°Ņ”Ņ‚ĐĩҁҌ, ĐŧĐžĐļŅƒŅ‚ŅŒ ĐąŅƒŅ‚Đ¸ виĐēĐģиĐēаĐŊŅ– Ņ†Đ¸Đŧ ĐŋаĐēĐĩŅ‚ĐžĐŧ, Ņ‚ĐžĐŧ҃ ҁĐŋĐžŅ‡Đ°Ņ‚Đē҃ СвĐĩŅ€ĐŊŅ–Ņ‚ŅŒŅŅ Đ´Đž ĐŊĐ¸Ņ… Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ, виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅŽŅ‡Đ¸ ĐŊавĐĩĐ´ĐĩĐŊŅ– ĐŊиĐļ҇Đĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ.", + "support_third_party_description": "Đ’Đ°ŅˆŅƒ ĐˇĐąŅ–Ņ€Đē҃ Immich ĐąŅƒĐģĐž ĐŋŅ–Đ´ĐŗĐžŅ‚ĐžĐ˛ĐģĐĩĐŊĐž ŅŅ‚ĐžŅ€ĐžĐŊĐŊŅ–Đŧ Ņ€ĐžĐˇŅ€ĐžĐąĐŊиĐēĐžĐŧ. ĐŸŅ€ĐžĐąĐģĐĩĐŧи ĐŧĐžĐļŅƒŅ‚ŅŒ ĐąŅƒŅ‚Đ¸ виĐēĐģиĐēаĐŊŅ– Ņ†Đ¸Đŧ ĐŋаĐēĐĩŅ‚ĐžĐŧ, Ņ‚ĐžĐŧ҃ ҁĐŋĐĩŅ€ŅˆŅƒ СвĐĩŅ€ĐŊŅ–Ņ‚ŅŒŅŅ Đ´Đž ĐšĐžĐŗĐž Đ°Đ˛Ņ‚ĐžŅ€Đ° Са ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧи ĐŊиĐļ҇Đĩ.", "supporter": "ĐŸŅ€Đ¸Ņ…Đ¸ĐģҌĐŊиĐē", "swap_merge_direction": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŊаĐŋŅ€ŅĐŧĐžĐē Ой'Ņ”Đ´ĐŊаĐŊĐŊŅ", "sync": "ХиĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇŅƒĐ˛Đ°Ņ‚Đ¸", "sync_albums": "ХиĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи", - "sync_albums_manual_subtitle": "ХиĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛ŅŅ– СаваĐŊŅ‚Đ°ĐļĐĩĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ҃ Đ˛Đ¸ĐąŅ€Đ°ĐŊŅ– аĐģŅŒĐąĐžĐŧи Đ´ĐģŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", + "sync_albums_manual_subtitle": "ХиĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛ŅŅ– виваĐŊŅ‚Đ°ĐļĐĩĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ҃ Đ˛Đ¸ĐąŅ€Đ°ĐŊŅ– аĐģŅŒĐąĐžĐŧи Đ´ĐģŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", "sync_local": "ХиĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", "sync_remote": "ХиĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ С ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ", "sync_status": "ĐĄŅ‚Đ°ĐŊ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ—", "sync_status_subtitle": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´ Ņ‚Đ° ĐēĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ŅĐ¸ŅŅ‚ĐĩĐŧĐžŅŽ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ—", - "sync_upload_album_setting_subtitle": "ĐĄŅ‚Đ˛ĐžŅ€ŅŽĐšŅ‚Đĩ Ņ‚Đ° виваĐŊŅ‚Đ°ĐļŅƒĐšŅ‚Đĩ ŅĐ˛ĐžŅ— Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž Đ´Đž Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€ Immich", + "sync_upload_album_setting_subtitle": "ĐĄŅ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ Ņ‚Đ° виваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ŅĐ˛ĐžŅ— Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž Đ´Đž Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€ Immich", "tag": "ĐĸĐĩĐŗ", "tag_assets": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Ņ‚ĐĩĐŗĐ¸", "tag_created": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž Ņ‚ĐĩĐŗ: {tag}", - "tag_feature_description": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž, ĐˇĐŗŅ€ŅƒĐŋОваĐŊĐ¸Ņ… Са ĐģĐžĐŗŅ–Ņ‡ĐŊиĐŧи Ņ‚ĐĩĐŧаĐŧи Ņ‚ĐĩĐŗŅ–Đ˛", - "tag_not_found_question": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŊĐ°ĐšŅ‚Đ¸ Ņ‚ĐĩĐŗ? ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊОвиК Ņ‚ĐĩĐŗ.", - "tag_people": "ĐĸĐĩĐŗ ĐģŅŽĐ´ĐĩĐš", + "tag_face": "ĐĸĐĩĐŗ ОйĐģĐ¸Ņ‡Ņ‡Ņ", + "tag_feature_description": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž, ĐˇĐŗŅ€ŅƒĐŋОваĐŊĐ¸Ņ… Са ĐģĐžĐŗŅ–Ņ‡ĐŊиĐŧи Ņ‚ĐĩĐŧаĐŧи Ņ‚ĐĩĐŗŅ–Đ˛", + "tag_not_found_question": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŊĐ°ĐšŅ‚Đ¸ Ņ‚ĐĩĐŗ? ĐĄŅ‚Đ˛ĐžŅ€Ņ–Ņ‚ŅŒ ĐŊОвиК Ņ‚ĐĩĐŗ.", + "tag_people": "ПозĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš", "tag_updated": "ОĐŊОвĐģĐĩĐŊĐž Ņ‚ĐĩĐŗ: {tag}", - "tagged_assets": "ПозĐŊĐ°Ņ‡ĐĩĐŊĐž Ņ‚ĐĩĐŗĐžĐŧ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "tagged_assets": "ПозĐŊĐ°Ņ‡ĐĩĐŊĐž Ņ‚ĐĩĐŗĐžĐŧ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", "tags": "ĐĸĐĩĐŗĐ¸", - "tap_to_run_job": "ĐĸĐžŅ€ĐēĐŊŅ–Ņ‚ŅŒŅŅ, Ņ‰ĐžĐą СаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ СавдаĐŊĐŊŅ", + "tap_to_run_job": "ĐĸĐžŅ€ĐēĐŊŅ–Ņ‚ŅŒŅŅ, Ņ‰ĐžĐą виĐēĐžĐŊĐ°Ņ‚Đ¸ СавдаĐŊĐŊŅ", "template": "ШайĐģĐžĐŊ", "text_recognition": "РОСĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ Ņ‚ĐĩĐēŅŅ‚Ņƒ", - "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": "ĐĸĐĩĐŧа ĐžŅ„ĐžŅ€ĐŧĐģĐĩĐŊĐŊŅ", + "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_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": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ‚Ņ€Đ¸ĐĩŅ‚Đ°ĐŋĐŊĐĩ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "theme_setting_theme_subtitle": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ‚ĐĩĐŧи ĐžŅ„ĐžŅ€ĐŧĐģĐĩĐŊĐŊŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃", + "theme_setting_three_stage_loading_subtitle": "ĐĸŅ€Đ¸ĐĩŅ‚Đ°ĐŋĐŊĐĩ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ ĐŋŅ–Đ´Đ˛Đ¸Ņ‰Đ¸Ņ‚Đ¸ ŅˆĐ˛Đ¸Đ´ĐēŅ–ŅŅ‚ŅŒ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ, аĐģĐĩ СĐŊĐ°Ņ‡ĐŊĐž ĐˇĐąŅ–ĐģŅŒŅˆĐ¸Ņ‚ŅŒ ĐŊаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŊа ĐŧĐĩŅ€ĐĩĐļ҃", + "theme_setting_three_stage_loading_title": "ĐĸŅ€Đ¸ĐĩŅ‚Đ°ĐŋĐŊĐĩ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", "then": "ĐĸĐžĐ´Ņ–", - "they_will_be_merged_together": "ВоĐŊи ĐąŅƒĐ´ŅƒŅ‚ŅŒ Ой'Ņ”Đ´ĐŊаĐŊŅ– Ņ€Đ°ĐˇĐžĐŧ", + "they_will_be_merged_together": "Đ‡Ņ… ĐąŅƒĐ´Đĩ Ой'Ņ”Đ´ĐŊаĐŊĐž", "third_party_resources": "ĐĄŅ‚ĐžŅ€ĐžĐŊĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸", "time": "Đ§Đ°Ņ", - "time_based_memories": "ĐĄĐŋĐžĐŗĐ°Đ´Đ¸, Ņ‰Đž ĐąĐ°ĐˇŅƒŅŽŅ‚ŅŒŅŅ ĐŊа Ņ‡Đ°ŅŅ–", + "time_based_memories": "ĐĄĐŋĐžĐŗĐ°Đ´Đ¸ Са Đ´Đ°Ņ‚ĐžŅŽ", "time_based_memories_duration": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ҁĐĩĐē҃ĐŊĐ´ Đ´ĐģŅ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐēĐžĐļĐŊĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ.", "timeline": "ĐĨŅ€ĐžĐŊĐžĐģĐžĐŗŅ–Ņ", "timezone": "Đ§Đ°ŅĐžĐ˛Đ¸Đš ĐŋĐžŅŅ", "to_archive": "ĐŅ€Ņ…Ņ–Đ˛", "to_change_password": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", - "to_favorite": "ĐžĐąŅ€Đ°ĐŊĐĩ", + "to_favorite": "Đ’Đ¸ĐąŅ€Đ°ĐŊĐĩ", "to_login": "Đ’Ņ…Ņ–Đ´", "to_multi_select": "Đ´ĐģŅ ĐŧĐŊĐžĐļиĐŊĐŊĐžĐŗĐž Đ˛Đ¸ĐąĐžŅ€Ņƒ", - "to_parent": "ПовĐĩŅ€ĐŊŅƒŅ‚Đ¸ŅŅŒ ĐŊаСад", + "to_parent": "До ĐąĐ°Ņ‚ŅŒĐēŅ–Đ˛ŅŅŒĐēĐžŅ— ĐŋаĐŋĐēи", "to_select": "Đ˛Đ¸ĐąŅ€Đ°Ņ‚Đ¸", "to_trash": "ĐšĐžŅˆĐ¸Đē", "toggle_settings": "ПĐĩŅ€ĐĩĐŧиĐēаĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ", - "toggle_theme_description": "ПĐĩŅ€ĐĩĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ‚ĐĩĐŧ҃", + "toggle_theme_description": "ПĐĩŅ€ĐĩĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ‚ĐĩĐŧ҃ ĐžŅ„ĐžŅ€ĐŧĐģĐĩĐŊĐŊŅ", "total": "ĐŖŅŅŒĐžĐŗĐž", "total_usage": "Đ—Đ°ĐŗĐ°ĐģҌĐŊĐĩ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ", "trash": "ĐšĐžŅˆĐ¸Đē", "trash_action_prompt": "{count} ĐŋĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа", - "trash_all": "ВидаĐģĐ¸Ņ‚Đ¸ Đ˛ŅĐĩ", - "trash_count": "ВидаĐģĐ¸Ņ‚Đ¸ {count, number}", - "trash_delete_asset": "ĐŖ ĐšĐžŅˆĐ¸Đē/ВидаĐģĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ", + "trash_all": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ Đ˛ŅĐĩ Đ´Đž ĐēĐžŅˆĐ¸Đēа", + "trash_count": "ĐšĐžŅˆĐ¸Đē {count, number}", + "trash_delete_asset": "ĐŖ ĐēĐžŅˆĐ¸Đē/ВидаĐģĐ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚", "trash_emptied": "ĐšĐžŅˆĐ¸Đē ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐž", - "trash_no_results_message": "ĐĸŅƒŅ‚ С'ŅĐ˛ĐģŅŅ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ видаĐģĐĩĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž.", + "trash_no_results_message": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž С ĐēĐžŅˆĐ¸Đēа С'ŅĐ˛ĐģŅŅ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ Ņ‚ŅƒŅ‚.", "trash_page_delete_all": "ВидаĐģĐ¸Ņ‚Đ¸ ҃ҁĐĩ", - "trash_page_empty_trash_dialog_content": "Ви Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐžŅˆĐ¸Đē? ĐĻŅ– Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊŅ– С Immich", + "trash_page_empty_trash_dialog_content": "ĐĨĐžŅ‡ĐĩŅ‚Đĩ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐžŅˆĐ¸Đē? ĐĻŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐąŅƒĐ´Đĩ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊĐž С Immich", "trash_page_info": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊŅ– Đ´Đž ĐēĐžŅˆĐ¸Đēа Ņ„Đ°ĐšĐģи ĐąŅƒĐ´Đĩ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊĐž ҇ĐĩŅ€ĐĩС {days} Đ´ĐŊŅ–Đ˛", - "trash_page_no_assets": "ВидаĐģĐĩĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ–", + "trash_page_no_assets": "НĐĩĐŧĐ°Ņ” ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ҃ ĐēĐžŅˆĐ¸Đē҃", "trash_page_restore_all": "Đ’Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ҃ҁĐĩ", - "trash_page_select_assets_btn": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", + "trash_page_select_assets_btn": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", "trash_page_title": "ĐšĐžŅˆĐ¸Đē ({count})", - "trashed_items_will_be_permanently_deleted_after": "ВидаĐģĐĩĐŊŅ– Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊŅ– ҇ĐĩŅ€ĐĩС {days, plural, one {# Đ´ĐĩĐŊҌ} few {# Đ´ĐŊŅ–} many {# Đ´ĐŊŅ–Đ˛} other {# Đ´ĐŊŅ–Đ˛}}.", + "trashed_items_will_be_permanently_deleted_after": "ЕĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ҃ ĐēĐžŅˆĐ¸Đē҃ ĐąŅƒĐ´Đĩ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊĐž ҇ĐĩŅ€ĐĩС {days, plural, one {# Đ´ĐĩĐŊҌ} few {# Đ´ĐŊŅ–} many {# Đ´ĐŊŅ–Đ˛} other {# Đ´ĐŊŅ–Đ˛}}.", "trigger": "ĐĸŅ€Đ¸ĐŗĐĩŅ€", - "trigger_asset_uploaded": "ФаКĐģ дОдаĐŊĐž", - "trigger_asset_uploaded_description": "ЗаĐŋ҃ҁĐēĐ°Ņ”Ņ‚ŅŒŅŅ ĐŋŅ–Đ´ Ņ‡Đ°Ņ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŊĐžĐ˛ĐžĐŗĐž Ņ„Đ°ĐšĐģ҃", + "trigger_asset_uploaded": "ЕĐģĐĩĐŧĐĩĐŊŅ‚ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐž", + "trigger_asset_uploaded_description": "ĐĄĐŋŅ€Đ°Ņ†ŅŒĐžĐ˛ŅƒŅ”, ĐēĐžĐģи виваĐŊŅ‚Đ°ĐļĐĩĐŊĐž ĐŊОвиК ĐĩĐģĐĩĐŧĐĩĐŊŅ‚", "trigger_description": "ĐŸĐžĐ´Ņ–Ņ, ŅĐēа СаĐŋ҃ҁĐēĐ°Ņ” Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–ŅŽ", - "trigger_person_recognized": "ĐžŅĐžĐąĐ° Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаĐŊа", + "trigger_person_recognized": "Đ›ŅŽĐ´Đ¸ĐŊ҃ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаĐŊĐž", "trigger_person_recognized_description": "ĐĄĐŋŅ€Đ°Ņ†ŅŒĐžĐ˛ŅƒŅ”, ĐēĐžĐģи Đ˛Đ¸ŅĐ˛ĐģŅŅ”Ņ‚ŅŒŅŅ ĐģŅŽĐ´Đ¸ĐŊа", "trigger_type": "ĐĸиĐŋ Ņ‚Ņ€Đ¸ĐŗĐĩŅ€Đ°", - "troubleshoot": "ВиĐŋŅ€Đ°Đ˛ĐģĐĩĐŊĐŊŅ ĐŊĐĩĐŋĐžĐģадОĐē", + "troubleshoot": "ĐŖŅŅƒĐŊĐĩĐŊĐŊŅ ĐŊĐĩĐŋĐžĐģадОĐē", "type": "ĐĸиĐŋ", - "unable_to_change_pin_code": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ PIN-ĐēОд", + "unable_to_change_pin_code": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ PIN-ĐēОд", "unable_to_check_version": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đ¸Ņ‚Đ¸ вĐĩŅ€ŅŅ–ŅŽ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ айО ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", - "unable_to_setup_pin_code": "НĐĩĐŧĐžĐļĐģивО ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°Ņ‚Đ¸ PIN-ĐēОд", - "unarchive": "Đ ĐžĐˇĐ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸", - "unarchive_action_prompt": "{count, plural, one {# Ņ„Đ°ĐšĐģ виĐģŅƒŅ‡ĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ} few {# Ņ„Đ°ĐšĐģи виĐģŅƒŅ‡ĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ} other {# Ņ„Đ°ĐšĐģŅ–Đ˛ виĐģŅƒŅ‡ĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ}}", - "unarchived_count": "{count, plural, other {ПовĐĩŅ€ĐŊŅƒŅ‚Đž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ #}}", + "unable_to_setup_pin_code": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°Ņ‚Đ¸ PIN-ĐēОд", + "unarchive": "ВиĐģŅƒŅ‡Đ¸Ņ‚Đ¸ С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ", + "unarchive_action_prompt": "{count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ виĐģŅƒŅ‡ĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ виĐģŅƒŅ‡ĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ виĐģŅƒŅ‡ĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ виĐģŅƒŅ‡ĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ}}", + "unarchived_count": "{count, plural, one {ВиĐģŅƒŅ‡ĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ #} few {ВиĐģŅƒŅ‡ĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ #} many {ВиĐģŅƒŅ‡ĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ #} other {ВиĐģŅƒŅ‡ĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ #}}", "undo": "ĐĄĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸", - "unfavorite": "ВидаĐģĐ¸Ņ‚Đ¸ С ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž", - "unfavorite_action_prompt": "{count} виĐģŅƒŅ‡ĐĩĐŊĐž С ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž", - "unhide_person": "РОСĐēŅ€Đ¸Ņ‚Đ¸ ĐžŅĐžĐąŅƒ", + "unfavorite": "ВиĐģŅƒŅ‡Đ¸Ņ‚Đ¸ С Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž", + "unfavorite_action_prompt": "{count} виĐģŅƒŅ‡ĐĩĐŊĐž С Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžĐŗĐž", + "unhide_person": "РОСĐēŅ€Đ¸Ņ‚Đ¸ ĐģŅŽĐ´Đ¸ĐŊ҃", "unknown": "НĐĩĐ˛Ņ–Đ´ĐžĐŧĐž", "unknown_country": "НĐĩĐ˛Ņ–Đ´ĐžĐŧа ĐēŅ€Đ°Ņ—ĐŊа", "unknown_date": "НĐĩĐ˛Ņ–Đ´ĐžĐŧа Đ´Đ°Ņ‚Đ°", @@ -2296,130 +2304,133 @@ "unlimited": "БĐĩС ОйĐŧĐĩĐļĐĩĐŊҌ", "unlink_motion_video": "Đ’Ņ–Đ´'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Ņ€ŅƒŅ…ĐžĐŧĐĩ Đ˛Ņ–Đ´ĐĩĐž", "unlink_oauth": "Đ’Ņ–Đ´'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ OAuth", - "unlinked_oauth_account": "Đ’Ņ–Đ´'Ņ”Đ´ĐŊаĐŊиК ОйĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ OAuth", - "unmute_memories": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐˇĐ˛ŅƒĐē ҁĐŋĐžĐŗĐ°Đ´Ņ–Đ˛", + "unlinked_oauth_account": "ОбĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ OAuth Đ˛Ņ–Đ´'Ņ”Đ´ĐŊаĐŊĐž", + "unmute_memories": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ҁĐŋĐžĐŗĐ°Đ´Đ¸", "unnamed_album": "АĐģŅŒĐąĐžĐŧ ĐąĐĩС ĐŊаСви", - "unnamed_album_delete_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž йаĐļĐ°Ņ”Ņ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ҆ĐĩĐš аĐģŅŒĐąĐžĐŧ?", + "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 {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "unselect_all": "ЗĐŊŅŅ‚Đ¸ Đ˛Đ¸ĐąŅ–Ņ€ С ŅƒŅŅ–Ņ…", + "unselect_all_duplicates": "ЗĐŊŅŅ‚Đ¸ Đ˛Đ¸ĐąŅ–Ņ€ С ŅƒŅŅ–Ņ… Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛", + "unselect_all_in": "ЗĐŊŅŅ‚Đ¸ Đ˛Đ¸ĐąŅ–Ņ€ ҃ {group}", + "unstack": "Đ ĐžĐˇĐŗŅ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸", + "unstack_action_prompt": "{count} — Ņ€ĐžĐˇĐŗŅ€ŅƒĐŋОваĐŊĐž", + "unstacked_assets_count": "Đ ĐžĐˇĐŗŅ€ŅƒĐŋОваĐŊĐž {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", "unsupported_field_type": "НĐĩĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒĐ˛Đ°ĐŊиК Ņ‚Đ¸Đŋ ĐŋĐžĐģŅ", + "unsupported_file_type": "ФаКĐģ {file} ĐŊĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ виваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸, ĐžŅĐēŅ–ĐģҌĐēи Ņ‚Đ¸Đŋ Ņ„Đ°ĐšĐģ҃ {type} ĐŊĐĩ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ”Ņ‚ŅŒŅŅ.", "untagged": "БĐĩС Ņ‚ĐĩĐŗŅ–Đ˛", - "untitled_workflow": "БĐĩĐˇŅ–ĐŧĐĩĐŊĐŊиК Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", + "untitled_workflow": "БĐĩĐˇŅ–ĐŧĐĩĐŊĐŊа Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–Ņ", "up_next": "ĐĐ°ŅŅ‚ŅƒĐŋĐŊĐĩ", - "update_location_action_prompt": "ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ€ĐžĐˇŅ‚Đ°ŅˆŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… ĐžĐąâ€™Ņ”ĐēŅ‚Ņ–Đ˛ ({count}) Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ:", + "update_location_action_prompt": "ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐŧҖҁ҆Đĩ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ({count}) Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ:", "updated_at": "ОĐŊОвĐģĐĩĐŊĐž", "updated_password": "ĐŸĐ°Ņ€ĐžĐģҌ ĐžĐŊОвĐģĐĩĐŊĐž", "upload": "ВиваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸", - "upload_concurrency": "ĐŸĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊŅ–ŅŅ‚ŅŒ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "upload_concurrency": "ОдĐŊĐžŅ‡Đ°ŅĐŊŅ– виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", "upload_details": "ДĐĩŅ‚Đ°ĐģŅ– виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", - "upload_dialog_info": "БаĐļĐ°Ņ”Ņ‚Đĩ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛ ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ?", - "upload_dialog_title": "ВиваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", - "upload_error_with_count": "ПоĐŧиĐģĐēа виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ Đ´ĐģŅ {count, plural, one {# Ņ„Đ°ĐšĐģ҃} few {# Ņ„Đ°ĐšĐģŅ–Đ˛} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", - "upload_errors": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ СавĐĩŅ€ŅˆĐĩĐŊĐž С {count, plural, one {# ĐŋĐžĐŧиĐģĐēĐžŅŽ} few {# ĐŋĐžĐŧиĐģĐēаĐŧи} many {# ĐŋĐžĐŧиĐģĐēаĐŧи} other {# ĐŋĐžĐŧиĐģĐēаĐŧи}}, ĐžĐŊĐžĐ˛Ņ–Ņ‚ŅŒ ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃, Ņ‰ĐžĐą ĐŋĐžĐąĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– виваĐŊŅ‚Đ°ĐļĐĩĐŊŅ– Ņ„Đ°ĐšĐģи.", + "upload_dialog_info": "ĐĨĐžŅ‡ĐĩŅ‚Đĩ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ?", + "upload_dialog_title": "ВиваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚", + "upload_error_with_count": "НĐĩ вдаĐģĐžŅŅ виваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "upload_errors": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ СавĐĩŅ€ŅˆĐĩĐŊĐž С {count, plural, one {# ĐŋĐžĐŧиĐģĐēĐžŅŽ} few {# ĐŋĐžĐŧиĐģĐēаĐŧи} many {# ĐŋĐžĐŧиĐģĐēаĐŧи} other {# ĐŋĐžĐŧиĐģĐēаĐŧи}}, ĐžĐŊĐžĐ˛Ņ–Ņ‚ŅŒ ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃, Ņ‰ĐžĐą ĐŋĐžĐąĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– виваĐŊŅ‚Đ°ĐļĐĩĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸.", "upload_finished": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ СавĐĩŅ€ŅˆĐĩĐŊĐž", - "upload_progress": "ЗаĐģĐ¸ŅˆĐ¸ĐģĐžŅŅŒ {remaining, number} - ОĐŋŅ€Đ°Ņ†ŅŒĐžĐ˛Đ°ĐŊĐž {processed, number}/{total, number}", - "upload_skipped_duplicates": "ĐŸŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž {count, plural, one {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊиК Ņ„Đ°ĐšĐģ} few {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– Ņ„Đ°ĐšĐģи} many {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "upload_progress": "ЗаĐģĐ¸ŅˆĐ¸ĐģĐžŅŅ {remaining, number} - ОĐŋŅ€Đ°Ņ†ŅŒĐžĐ˛Đ°ĐŊĐž {processed, number}/{total, number}", + "upload_skipped_duplicates": "ĐŸŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž {count, plural, one {# Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚} few {# Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸} many {# Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛} other {# Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛}}", "upload_status_duplicates": "Đ”ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸", "upload_status_errors": "ПоĐŧиĐģĐēи", "upload_status_uploaded": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐž", - "upload_success": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ҃ҁĐŋŅ–ŅˆĐŊĐĩ. ОĐŊĐžĐ˛Ņ–Ņ‚ŅŒ ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃, Ņ‰ĐžĐą ĐŋĐžĐąĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– виваĐŊŅ‚Đ°ĐļĐĩĐŊŅ– Ņ„Đ°ĐšĐģи.", + "upload_success": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐž. ОĐŊĐžĐ˛Ņ–Ņ‚ŅŒ ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃, Ņ‰ĐžĐą ĐŋĐžĐąĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸.", "upload_to_immich": "ВиваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ в Immich ({count})", "uploading": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", - "uploading_media": "ВиĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "uploading_media": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŧĐĩĐ´Ņ–Đ°", "url": "URL", "usage": "ВиĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ", "use_biometric": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐąŅ–ĐžĐŧĐĩŅ‚Ņ€Ņ–ŅŽ", "use_browser_locale": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐģĐžĐēаĐģҌ ĐąŅ€Đ°ŅƒĐˇĐĩŅ€Đ°", + "use_browser_locale_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Đ¸, Ņ‡Đ°Ņ Ņ‚Đ° Ņ‡Đ¸ŅĐģа Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´ĐŊĐž Đ´Đž ĐģĐžĐēаĐģŅ– Đ˛Đ°ŅˆĐžĐŗĐž ĐąŅ€Đ°ŅƒĐˇĐĩŅ€Đ°", "use_current_connection": "ВиĐēĐžŅ€Đ¸ŅŅ‚Đ°Ņ‚Đ¸ ĐŋĐžŅ‚ĐžŅ‡ĐŊĐĩ С'Ņ”Đ´ĐŊаĐŊĐŊŅ", - "use_custom_date_range": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ†ŅŒĐēиК Đ´Ņ–Đ°ĐŋаСОĐŊ Đ´Đ°Ņ‚", + "use_custom_date_range": "ĐĐ°Ņ‚ĐžĐŧŅ–ŅŅ‚ŅŒ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Đ´ĐžĐ˛Ņ–ĐģҌĐŊиК Đ´Ņ–Đ°ĐŋаСОĐŊ Đ´Đ°Ņ‚", "user": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡", "user_has_been_deleted": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° видаĐģĐĩĐŊĐž.", - "user_id": "ID ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "user_liked": "{user} вĐŋОдОйав {type, select, photo {҆Đĩ Ņ„ĐžŅ‚Đž} video {҆Đĩ Đ˛Ņ–Đ´ĐĩĐž} asset {҆ĐĩĐš Ņ„Đ°ĐšĐģ} other {҆Đĩ}}", + "user_id": "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_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 {# ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°} few {# ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–} many {# ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛} other {# ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛}} дОдаĐŊĐž Đ´Đž аĐģŅŒĐąĐžĐŧ҃", + "users_added_to_album_count": "{count, plural, one {# ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°} few {# ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛} many {# ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛} other {# ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛}} дОдаĐŊĐž Đ´Đž аĐģŅŒĐąĐžĐŧ҃", "utilities": "ĐŖŅ‚Đ¸ĐģŅ–Ņ‚Đ¸", "validate": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đ¸Ņ‚Đ¸", "validate_endpoint_error": "Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ввĐĩĐ´Ņ–Ņ‚ŅŒ Đ´Ņ–ĐšŅĐŊ҃ URL-Đ°Đ´Ņ€Đĩҁ҃", "validation_error": "ПоĐŧиĐģĐēа ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēи", "variables": "ЗĐŧŅ–ĐŊĐŊŅ–", "version": "ВĐĩŅ€ŅŅ–Ņ", - "version_announcement_closing": "ĐĸĐ˛Ņ–Đš Đ´Ņ€ŅƒĐŗ, АĐģĐĩĐēҁ", - "version_announcement_message": "ĐŸŅ€Đ¸Đ˛Ņ–Ņ‚! Đ”ĐžŅŅ‚ŅƒĐŋĐŊа ĐŊОва вĐĩŅ€ŅŅ–Ņ Immich. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐŋŅ€Đ¸Đ´Ņ–ĐģŅ–Ņ‚ŅŒ Ņ‚Ņ€ĐžŅ…Đ¸ Ņ‡Đ°ŅŅƒ Đ´ĐģŅ ОСĐŊаКОĐŧĐģĐĩĐŊĐŊŅ С ĐŋŅ€Đ¸ĐŧŅ–Ņ‚ĐēаĐŧи Đ´Đž виĐŋ҃ҁĐē҃, Ņ‰ĐžĐą ĐŋĐĩŅ€ĐĩĐēĐžĐŊĐ°Ņ‚Đ¸ŅŅ, Ņ‰Đž Đ˛Đ°ŅˆĐ° ŅƒŅŅ‚Đ°ĐŊОвĐēа ĐžĐŊОвĐģĐĩĐŊа Ņ– ҃ĐŊиĐēĐŊŅƒŅ‚Đ¸ ĐŧĐžĐļĐģĐ¸Đ˛Đ¸Ņ… ĐŋĐžĐŧиĐģĐžĐē ҃ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ…, ĐžŅĐžĐąĐģивО ŅĐēŅ‰Đž ви виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ WatchTower айО ĐąŅƒĐ´ŅŒ-ŅĐēиК Ņ–ĐŊŅˆĐ¸Đš ĐŧĐĩŅ…Đ°ĐŊŅ–ĐˇĐŧ, ŅĐēиК Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž ĐžĐŊОвĐģŅŽŅ” Đ˛Đ°Ņˆ ĐĩĐēСĐĩĐŧĐŋĐģŅŅ€ Immich.", + "version_announcement_closing": "Đ’Đ°Ņˆ Đ´Ņ€ŅƒĐŗ, АĐģĐĩĐēҁ", + "version_announcement_message": "ĐŸŅ€Đ¸Đ˛Ņ–Ņ‚! Đ”ĐžŅŅ‚ŅƒĐŋĐŊа ĐŊОва вĐĩŅ€ŅŅ–Ņ Immich. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐŋŅ€Đ¸Đ´Ņ–ĐģŅ–Ņ‚ŅŒ Ņ‚Ņ€ĐžŅ…Đ¸ Ņ‡Đ°ŅŅƒ Ņ‰ĐžĐą ОСĐŊаКОĐŧĐ¸Ņ‚Đ¸ŅŅ С ĐŋŅ€Đ¸ĐŧŅ–Ņ‚ĐēаĐŧи Đ´Đž виĐŋ҃ҁĐē҃, Ņ‰ĐžĐą ĐŋĐĩŅ€ĐĩĐēĐžĐŊĐ°Ņ‚Đ¸ŅŅ, Ņ‰Đž Đ˛Đ°ŅˆŅƒ ŅƒŅŅ‚Đ°ĐŊОвĐē҃ ĐžĐŊОвĐģĐĩĐŊĐž Đš ҃ĐŊиĐēĐŊŅƒŅ‚Đ¸ ĐŧĐžĐļĐģĐ¸Đ˛Đ¸Ņ… ĐŋĐžĐŧиĐģĐžĐē ҃ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ…, ĐžŅĐžĐąĐģивО ŅĐēŅ‰Đž ви виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ WatchTower айО ĐąŅƒĐ´ŅŒ-ŅĐēиК Ņ–ĐŊŅˆĐ¸Đš ĐŧĐĩŅ…Đ°ĐŊŅ–ĐˇĐŧ, ŅĐēиК Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž ĐžĐŊОвĐģŅŽŅ” Đ˛Đ°Ņˆ ĐĩĐēСĐĩĐŧĐŋĐģŅŅ€ Immich.", "version_history": "Đ†ŅŅ‚ĐžŅ€Ņ–Ņ вĐĩŅ€ŅŅ–Đš", - "version_history_item": "Đ’ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž {version} {date}", + "version_history_item": "ĐŖŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž {version} — {date}", "video": "Đ’Ņ–Đ´ĐĩĐž", "video_hover_setting": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Đ˛Ņ–Đ´ĐĩĐž ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŊавĐĩĐ´ĐĩĐŊĐŊŅ ĐēŅƒŅ€ŅĐžŅ€Ņƒ ĐŧĐ¸ŅˆŅ–", - "video_hover_setting_description": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ Đ˛Ņ–Đ´ĐĩĐž ĐŋŅ€Đ¸ ĐŊавĐĩĐ´ĐĩĐŊĐŊŅ– ĐēŅƒŅ€ŅĐžŅ€Đ° ĐŊа Ņ„Đ°ĐšĐģ. ĐĐ°Đ˛Ņ–Ņ‚ŅŒ ŅĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž, Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ ĐąŅƒŅ‚Đ¸ СаĐŋŅƒŅ‰ĐĩĐŊĐž, ĐŊĐ°Đ˛Ņ–Đ˛ŅˆĐ¸ ĐēŅƒŅ€ŅĐžŅ€ ĐŊа ĐŋŅ–ĐēŅ‚ĐžĐŗŅ€Đ°Đŧ҃ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ.", + "video_hover_setting_description": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Ņƒ Đ˛Ņ–Đ´ĐĩĐž ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŊавĐĩĐ´ĐĩĐŊĐŊŅ ĐēŅƒŅ€ŅĐžŅ€Đ° ĐŊа ĐĩĐģĐĩĐŧĐĩĐŊŅ‚. ĐĐ°Đ˛Ņ–Ņ‚ŅŒ ŅĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž, Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŧĐžĐļĐŊа Ņ€ĐžĐˇĐŋĐžŅ‡Đ°Ņ‚Đ¸, ĐŊĐ°Đ˛Ņ–Đ˛ŅˆĐ¸ ĐēŅƒŅ€ŅĐžŅ€ ĐŊа ĐŋŅ–ĐēŅ‚ĐžĐŗŅ€Đ°Đŧ҃ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ.", "videos": "Đ’Ņ–Đ´ĐĩĐž", - "videos_count": "{count, plural, one {# Đ’Ņ–Đ´ĐĩĐž} few {# Đ’Ņ–Đ´ĐĩĐž} many {# Đ’Ņ–Đ´ĐĩĐž} other {# Đ’Ņ–Đ´ĐĩĐž}}", - "videos_only": "ĐĸŅ–ĐģҌĐēи Đ˛Ņ–Đ´ĐĩĐž", + "videos_count": "{count, plural, one {# Đ˛Ņ–Đ´ĐĩĐž} few {# Đ˛Ņ–Đ´ĐĩĐž} many {# Đ˛Ņ–Đ´ĐĩĐž} other {# Đ˛Ņ–Đ´ĐĩĐž}}", + "videos_only": "Đ›Đ¸ŅˆĐĩ Đ˛Ņ–Đ´ĐĩĐž", "view": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´", "view_album": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ аĐģŅŒĐąĐžĐŧ", - "view_all": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ŅƒŅŅ–", + "view_all": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ Đ˛ŅĐĩ", "view_all_users": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ Đ˛ŅŅ–Ņ… ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛", - "view_asset_owners": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ вĐģĐ°ŅĐŊиĐēŅ–Đ˛ Ņ„Đ°ĐšĐģŅ–Đ˛", + "view_asset_owners": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ вĐģĐ°ŅĐŊиĐēŅ–Đ˛ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", "view_details": "ДĐĩŅ‚Đ°ĐģҌĐŊŅ–ŅˆĐĩ", "view_in_timeline": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ в Ņ…Ņ€ĐžĐŊĐžĐģĐžĐŗŅ–Ņ—", "view_link": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "view_links": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "view_name": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸", - "view_next_asset": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊиК Ņ„Đ°ĐšĐģ", - "view_previous_asset": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅ–Đš Ņ„Đ°ĐšĐģ", + "view_next_asset": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊиК ĐĩĐģĐĩĐŧĐĩĐŊŅ‚", + "view_previous_asset": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅ–Đš ĐĩĐģĐĩĐŧĐĩĐŊŅ‚", "view_qr_code": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ QR-ĐēОд", - "view_similar_photos": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ŅŅ…ĐžĐļŅ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—", - "view_stack": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´ ҁ҂ĐĩĐē҃", + "view_similar_photos": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ŅŅ…ĐžĐļŅ– Ņ„ĐžŅ‚Đž", + "view_stack": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ҁ҂ĐĩĐē", "view_user": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "viewer_remove_from_stack": "ВидаĐģĐ¸Ņ‚Đ¸ ĐˇŅ– ҁ҂ĐĩĐē҃", - "viewer_stack_use_as_main_asset": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ŅĐē ĐžŅĐŊОвĐŊиК Ņ„Đ°ĐšĐģ", - "viewer_unstack": "Đ ĐžĐˇŅ–ĐąŅ€Đ°Ņ‚Đ¸ ҁ҂ĐĩĐē", - "visibility_changed": "ВидиĐŧŅ–ŅŅ‚ŅŒ СĐŧŅ–ĐŊĐĩĐŊĐž Đ´ĐģŅ {count, plural, one {# ĐžŅĐžĐąĐ¸} few {# ĐžŅŅ–Đą} many {# ĐžŅŅ–Đą} other {# ĐžŅŅ–Đą}}", + "viewer_remove_from_stack": "ВиĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ĐˇŅ– ҁ҂ĐĩĐē҃", + "viewer_stack_use_as_main_asset": "ВиĐēĐžŅ€Đ¸ŅŅ‚Đ°Ņ‚Đ¸ ŅĐē ĐžŅĐŊОвĐŊиК ĐĩĐģĐĩĐŧĐĩĐŊŅ‚", + "viewer_unstack": "Đ ĐžĐˇĐąĐ¸Ņ‚Đ¸ ҁ҂ĐĩĐē", + "visibility": "ВидиĐŧŅ–ŅŅ‚ŅŒ", + "visibility_changed": "ВидиĐŧŅ–ŅŅ‚ŅŒ СĐŧŅ–ĐŊĐĩĐŊĐž Đ´ĐģŅ {count, plural, one {# ĐģŅŽĐ´Đ¸ĐŊи} few {# ĐģŅŽĐ´ĐĩĐš} many {# ĐģŅŽĐ´ĐĩĐš} other {# ĐģŅŽĐ´ĐĩĐš}}", "visual": "Đ’Ņ–ĐˇŅƒĐ°ĐģҌĐŊиК", "visual_builder": "Đ’Ņ–ĐˇŅƒĐ°ĐģҌĐŊиК ĐēĐžĐŊŅŅ‚Ņ€ŅƒĐēŅ‚ĐžŅ€", "waiting": "ĐŖ ҇ĐĩŅ€ĐˇŅ–", - "waiting_count": "ĐžŅ‡Ņ–ĐēŅƒŅŽŅ‚ŅŒ: {count}", + "waiting_count": "ĐŖ ҇ĐĩŅ€ĐˇŅ–: {count}", "warning": "ПоĐŋĐĩŅ€ĐĩĐ´ĐļĐĩĐŊĐŊŅ", "week": "ĐĸиĐļĐ´ĐĩĐŊҌ", "welcome": "Đ›Đ°ŅĐēавО ĐŋŅ€ĐžŅĐ¸ĐŧĐž", "welcome_to_immich": "Đ›Đ°ŅĐēавО ĐŋŅ€ĐžŅĐ¸ĐŧĐž Đ´Đž Immich", "width": "Đ¨Đ¸Ņ€Đ¸ĐŊа", "wifi_name": "Назва Wi-Fi", - "workflow_delete_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ҆ĐĩĐš Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ?", - "workflow_deleted": "Đ ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ видаĐģĐĩĐŊĐž", - "workflow_description": "ОĐŋĐ¸Ņ Ņ€ĐžĐąĐžŅ‡ĐžĐŗĐž ĐŋŅ€ĐžŅ†Đĩҁ҃", - "workflow_info": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", - "workflow_json": "Đ ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ JSON", - "workflow_json_help": "Đ’Ņ–Đ´Ņ€ĐĩĐ´Đ°ĐŗŅƒĐšŅ‚Đĩ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ Ņ€ĐžĐąĐžŅ‡ĐžĐŗĐž ĐŋŅ€ĐžŅ†Đĩҁ҃ ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ– JSON. ЗĐŧŅ–ĐŊи ĐąŅƒĐ´ŅƒŅ‚ŅŒ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐžĐ˛Đ°ĐŊŅ– С Đ˛Ņ–ĐˇŅƒĐ°ĐģҌĐŊиĐŧ ĐēĐžĐŊŅŅ‚Ņ€ŅƒĐēŅ‚ĐžŅ€ĐžĐŧ.", - "workflow_name": "Назва Ņ€ĐžĐąĐžŅ‡ĐžĐŗĐž ĐŋŅ€ĐžŅ†Đĩҁ҃", + "workflow_delete_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ Ņ†ŅŽ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–ŅŽ?", + "workflow_deleted": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–ŅŽ видаĐģĐĩĐŊĐž", + "workflow_description": "ОĐŋĐ¸Ņ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–Ņ—", + "workflow_info": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–ŅŽ", + "workflow_json": "JSON Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–Ņ—", + "workflow_json_help": "Đ’Ņ–Đ´Ņ€ĐĩĐ´Đ°ĐŗŅƒĐšŅ‚Đĩ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–Ņ— ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ– JSON. ЗĐŧŅ–ĐŊи ĐąŅƒĐ´Đĩ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐžĐ˛Đ°ĐŊĐž С Đ˛Ņ–ĐˇŅƒĐ°ĐģҌĐŊиĐŧ ĐēĐžĐŊŅŅ‚Ņ€ŅƒĐēŅ‚ĐžŅ€ĐžĐŧ.", + "workflow_name": "Назва Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–Ņ—", "workflow_navigation_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Đ˛Đ¸ĐšŅ‚Đ¸ ĐąĐĩС СйĐĩŅ€ĐĩĐļĐĩĐŊĐŊŅ СĐŧŅ–ĐŊ?", - "workflow_summary": "ЗвĐĩĐ´ĐĩĐŊĐŊŅ Ņ€ĐžĐąĐžŅ‡ĐžĐŗĐž ĐŋŅ€ĐžŅ†Đĩҁ҃", - "workflow_update_success": "Đ ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ ҃ҁĐŋŅ–ŅˆĐŊĐž ĐžĐŊОвĐģĐĩĐŊĐž", - "workflow_updated": "Đ ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ ĐžĐŊОвĐģĐĩĐŊĐž", - "workflows": "Đ ĐžĐąĐžŅ‡Ņ– ĐŋŅ€ĐžŅ†ĐĩŅĐ¸", - "workflows_help_text": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–Ņ— виĐēĐžĐŊŅƒŅŽŅ‚ŅŒ Đ´Ņ–Ņ— С Ņ„Đ°ĐšĐģаĐŧи СаĐģĐĩĐļĐŊĐž Đ˛Ņ–Đ´ Ņ‚Ņ€Đ¸ĐŗĐĩŅ€Ņ–Đ˛ Ņ– ҃ĐŧОв", + "workflow_summary": "ЗвĐĩĐ´ĐĩĐŊĐŊŅ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–Ņ—", + "workflow_update_success": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–ŅŽ ĐžĐŊОвĐģĐĩĐŊĐž", + "workflow_updated": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–ŅŽ ĐžĐŊОвĐģĐĩĐŊĐž", + "workflows": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–Ņ—", + "workflows_help_text": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–Ņ— виĐēĐžĐŊŅƒŅŽŅ‚ŅŒ Đ´Ņ–Ņ— С ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°Đŧи СаĐģĐĩĐļĐŊĐž Đ˛Ņ–Đ´ Ņ‚Ņ€Đ¸ĐŗĐĩŅ€Ņ–Đ˛ Ņ– ҄ҖĐģŅŒŅ‚Ņ€Ņ–Đ˛", "wrong_pin_code": "НĐĩĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊиК PIN-ĐēОд", "year": "Đ Ņ–Đē", "years_ago": "{years, plural, one {# ҀҖĐē} few {# Ņ€ĐžĐēи} many {# Ņ€ĐžĐēŅ–Đ˛} other {# Ņ€ĐžĐēŅ–Đ˛}} Ņ‚ĐžĐŧ҃", "yes": "ĐĸаĐē", "you_dont_have_any_shared_links": "ĐŖ Đ˛Đ°Ņ ĐŊĐĩĐŧĐ°Ņ” ҁĐŋŅ–ĐģҌĐŊĐ¸Ņ… ĐŋĐžŅĐ¸ĐģаĐŊҌ", "your_wifi_name": "Назва Đ˛Đ°ŅˆĐžŅ— Wi-Fi ĐŧĐĩŅ€ĐĩĐļŅ–", - "zero_to_clear_rating": "ĐŊĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ 0, Ņ‰ĐžĐą ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ Ņ„Đ°ĐšĐģ҃", + "zero_to_clear_rating": "ĐŊĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ 0, Ņ‰ĐžĐą ҁĐēиĐŊŅƒŅ‚Đ¸ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°", "zoom_image": "Đ—ĐąŅ–ĐģŅŒŅˆĐ¸Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "zoom_to_bounds": "Đ—ĐąŅ–ĐģŅŒŅˆĐ¸Ņ‚Đ¸ ĐŧĐ°ŅŅˆŅ‚Đ°Đą Đ´Đž ĐŧĐĩĐļ" } diff --git a/i18n/vi.json b/i18n/vi.json index e9d5fb4006..5c3ace5da0 100644 --- a/i18n/vi.json +++ b/i18n/vi.json @@ -441,7 +441,7 @@ "user_successfully_removed": "Ngưáģi dÚng {email} Ä‘ÃŖ đưáģŖc xÃŗa thành công.", "users_page_description": "Trang quáēŖn tráģ‹ ngưáģi dÚng", "version_check_enabled_description": "Báē­t kiáģƒm tra phiÃĒn báēŖn", - "version_check_implications": "Tính năng kiáģƒm tra phiÃĒn báēŖn yÃĒu cáē§u káēŋt náģ‘i thưáģng xuyÃĒn đáēŋn github.com", + "version_check_implications": "Tính năng kiáģƒm tra phiÃĒn báēŖn yÃĒu cáē§u káēŋt náģ‘i thưáģng xuyÃĒn đáēŋn {server}", "version_check_settings": "Kiáģƒm tra phiÃĒn báēŖn", "version_check_settings_description": "Báē­t/táē¯t thông bÃĄo phiÃĒn báēŖn máģ›i", "video_conversion_job": "Chuyáģƒn mÃŖ video", @@ -537,10 +537,10 @@ "app_bar_signout_dialog_content": "BáēĄn cÃŗ muáģ‘n đăng xuáēĨt?", "app_bar_signout_dialog_ok": "CÃŗ", "app_bar_signout_dialog_title": "Đăng xuáēĨt", - "app_download_links": "LiÃĒn káēŋt táēŖi app", - "app_settings": "App", - "app_stores": "Cáģ­a hàng app", - "app_update_available": "ÄÃŖ cÃŗ báēŖn cáē­p nháē­t app", + "app_download_links": "LiÃĒn káēŋt táēŖi áģŠng dáģĨng", + "app_settings": "áģ¨ng dáģĨng", + "app_stores": "Cáģ­a hàng áģŠng dáģĨng", + "app_update_available": "ÄÃŖ cÃŗ báēŖn cáē­p nháē­t áģŠng dáģĨng", "appears_in": "XuáēĨt hiáģ‡n trong", "apply_count": "Áp dáģĨng ({count, number})", "archive": "Lưu tráģ¯", @@ -617,7 +617,7 @@ "back_close_deselect": "Quay láēĄi, Ä‘Ãŗng, hoáēˇc báģ cháģn", "background_backup_running_error": "Sao lưu náģn hiáģ‡n đang cháēĄy, không tháģƒ báē¯t đáē§u sao lưu tháģ§ công", "background_location_permission": "Quyáģn truy cáē­p váģ‹ trí khi cháēĄy náģn", - "background_location_permission_content": "Đáģƒ chuyáģƒn đáģ•i máēĄng khi cháēĄy áģŸ cháēŋ đáģ™ náģn, Immich *luôn* pháēŖi cÃŗ quyáģn truy cáē­p váģ‹ trí chính xÃĄc đáģƒ cÃŗ tháģƒ Ä‘áģc tÃĒn máēĄng Wi-Fi", + "background_location_permission_content": "Đáģƒ chuyáģƒn đáģ•i máēĄng khi cháēĄy áģŸ cháēŋ đáģ™ náģn, Immich pháēŖi *luôn* cÃŗ quyáģn truy cáē­p váģ‹ trí chính xÃĄc đáģƒ cÃŗ tháģƒ Ä‘áģc tÃĒn máēĄng Wi-Fi", "background_options": "TÚy cháģn náģn", "backup": "Sao lưu", "backup_album_selection_page_albums_device": "Album trÃĒn thiáēŋt báģ‹ ({count})", @@ -637,8 +637,8 @@ "backup_background_service_in_progress_notification": "Đang sao lưu táģ‡p cáģ§a báēĄnâ€Ļ", "backup_background_service_upload_failure_notification": "TáēŖi lÃĒn {filename} tháēĨt báēĄi", "backup_controller_page_albums": "Album sao lưu", - "backup_controller_page_background_app_refresh_disabled_content": "Báē­t làm máģ›i app trong náģn táēĄi Cài đáēˇt > Cài đáēˇt chung > Làm máģ›i app trong náģn đáģƒ dÚng sao lưu náģn.", - "backup_controller_page_background_app_refresh_disabled_title": "Làm máģ›i app trong náģn báģ‹ vô hiáģ‡u hoÃĄ", + "backup_controller_page_background_app_refresh_disabled_content": "Báē­t làm máģ›i áģŠng dáģĨng trong náģn táēĄi Cài đáēˇt > Cài đáēˇt chung > Làm máģ›i áģŠng dáģĨng trong náģn đáģƒ dÚng sao lưu náģn.", + "backup_controller_page_background_app_refresh_disabled_title": "Làm máģ›i áģŠng dáģĨng trong náģn báģ‹ vô hiáģ‡u hoÃĄ", "backup_controller_page_background_app_refresh_enable_button_text": "Đi táģ›i cài đáēˇt", "backup_controller_page_background_battery_info_link": "Hưáģ›ng dáēĢn tôi", "backup_controller_page_background_battery_info_message": "Đáģƒ cÃŗ tráēŖi nghiáģ‡m sao lưu náģn táģ‘t nháēĨt, vui lÃ˛ng vô hiáģ‡u hÃŗa báēĨt káģŗ táģ‘i ưu hÃŗa pin nào đang háēĄn cháēŋ hoáēĄt đáģ™ng náģn cáģ§a Immich.\n\nVÃŦ điáģu này pháģĨ thuáģ™c vào thiáēŋt báģ‹, vui lÃ˛ng tham kháēŖo thông tin cáē§n thiáēŋt cáģ§a nhà sáēŖn xuáēĨt thiáēŋt báģ‹ cáģ§a báēĄn.", @@ -647,7 +647,7 @@ "backup_controller_page_background_charging": "Cháģ‰ khi đang sáēĄc", "backup_controller_page_background_configure_error": "CáēĨu hÃŦnh dáģ‹ch váģĨ náģn tháēĨt báēĄi", "backup_controller_page_background_delay": "TrÃŦ hoÃŖn sao lưu táģ‡p máģ›i: {duration}", - "backup_controller_page_background_description": "Báē­t dáģ‹ch váģĨ náģn đáģƒ táģą Ä‘áģ™ng sao lưu táģ‡p máģ›i mà không cáē§n máģŸ app", + "backup_controller_page_background_description": "Báē­t dáģ‹ch váģĨ náģn đáģƒ táģą Ä‘áģ™ng sao lưu táģ‡p máģ›i mà không cáē§n máģŸ áģŠng dáģĨng", "backup_controller_page_background_is_off": "Sao lưu táģą Ä‘áģ™ng trong náģn đang táē¯t", "backup_controller_page_background_is_on": "Sao lưu táģą Ä‘áģ™ng trong náģn đang báē­t", "backup_controller_page_background_turn_off": "Táē¯t dáģ‹ch váģĨ náģn", @@ -657,7 +657,7 @@ "backup_controller_page_backup_selected": "ÄÃŖ cháģn: ", "backup_controller_page_backup_sub": "áēĸnh và video Ä‘ÃŖ sao lưu", "backup_controller_page_created": "TáēĄo vào: {date}", - "backup_controller_page_desc_backup": "Báē­t sao lưu khi app hoáēĄt đáģ™ng đáģƒ táģą Ä‘áģ™ng sao lưu táģ‡p máģ›i lÃĒn mÃĄy cháģ§ khi máģŸ app.", + "backup_controller_page_desc_backup": "Báē­t sao lưu khi áģŠng dáģĨng hoáēĄt đáģ™ng đáģƒ táģą Ä‘áģ™ng sao lưu táģ‡p máģ›i lÃĒn mÃĄy cháģ§ khi máģŸ áģŠng dáģĨng.", "backup_controller_page_excluded": "ÄÃŖ báģ qua: ", "backup_controller_page_failed": "TháēĨt báēĄi ({count})", "backup_controller_page_filename": "TÃĒn táģ‡p: {filename} [{size}]", @@ -668,12 +668,12 @@ "backup_controller_page_remainder_sub": "Sáģ‘ lưáģŖng áēŖnh và video Ä‘ÃŖ cháģn chưa đưáģŖc sao lưu", "backup_controller_page_server_storage": "Dung lưáģŖng mÃĄy cháģ§", "backup_controller_page_start_backup": "Báē¯t đáē§u sao lưu", - "backup_controller_page_status_off": "Sao lưu táģą Ä‘áģ™ng khi app hoáēĄt đáģ™ng đang táē¯t", - "backup_controller_page_status_on": "Sao lưu táģą Ä‘áģ™ng khi app hoáēĄt đáģ™ng đang báē­t", + "backup_controller_page_status_off": "Sao lưu táģą Ä‘áģ™ng khi áģŠng dáģĨng hoáēĄt đáģ™ng đang táē¯t", + "backup_controller_page_status_on": "Sao lưu táģą Ä‘áģ™ng khi áģŠng dáģĨng hoáēĄt đáģ™ng đang báē­t", "backup_controller_page_storage_format": "ÄÃŖ dÚng {used} cáģ§a {total}", "backup_controller_page_to_backup": "CÃĄc album cáē§n đưáģŖc sao lưu", "backup_controller_page_total_sub": "TáēĨt cáēŖ áēŖnh và video không trÚng láē­p táģĢ cÃĄc album đưáģŖc cháģn", - "backup_controller_page_turn_off": "Táē¯t sao lưu khi app hoáēĄt đáģ™ng", + "backup_controller_page_turn_off": "Táē¯t sao lưu khi áģŠng dáģĨng hoáēĄt đáģ™ng", "backup_controller_page_turn_on": "Báē­t sao lưu khi máģŸ app", "backup_controller_page_uploading_file_info": "Thông tin táģ‡p đang táēŖi lÃĒn", "backup_err_only_album": "Không tháģƒ xÃŗa album duy nháēĨt", @@ -704,16 +704,16 @@ "bulk_trash_duplicates_confirmation": "BáēĄn cÃŗ cháē¯c muáģ‘n đưa {count, plural, one {# táģ‡p trÚng láēˇp} other {# táģ‡p trÚng láēˇp}} vào thÚng rÃĄc? Điáģu này sáēŊ giáģ¯ láēĄi áēŖnh cháēĨt lưáģŖng nháēĨt cáģ§a máģ—i nhÃŗm và đưa táēĨt cáēŖ cÃĄc báēŖn trÚng láēˇp khÃĄc vào thÚng rÃĄc.", "buy": "Mua Immich", "cache_settings_clear_cache_button": "XÃŗa báģ™ nháģ› Ä‘áģ‡m", - "cache_settings_clear_cache_button_title": "XÃŗa báģ™ nháģ› Ä‘áģ‡m cáģ§a app. Điáģu này sáēŊ áēŖnh hưáģŸng đáēŋn hiáģ‡u suáēĨt cáģ§a app đáēŋn khi báģ™ nháģ› Ä‘áģ‡m đưáģŖc táēĄo láēĄi.", + "cache_settings_clear_cache_button_title": "XÃŗa báģ™ nháģ› Ä‘áģ‡m cáģ§a áģŠng dáģĨng. Điáģu này sáēŊ áēŖnh hưáģŸng đáēŋn hiáģ‡u suáēĨt cáģ§a áģŠng dáģĨng đáēŋn khi báģ™ nháģ› Ä‘áģ‡m đưáģŖc táēĄo láēĄi.", "cache_settings_duplicated_assets_clear_button": "XÓA", - "cache_settings_duplicated_assets_subtitle": "áēĸnh và video không đưáģŖc phÊp hiáģƒn tháģ‹ trÃĒn app", + "cache_settings_duplicated_assets_subtitle": "áēĸnh và video không đưáģŖc phÊp hiáģƒn tháģ‹ trÃĒn áģŠng dáģĨng", "cache_settings_duplicated_assets_title": "Táģ‡p báģ‹ trÚng ({count})", "cache_settings_statistics_album": "áēĸnh thu nháģ thư viáģ‡n", "cache_settings_statistics_full": "áēĸnh đáē§y đáģ§", "cache_settings_statistics_shared": "áēĸnh thu nháģ album chia sáēģ", "cache_settings_statistics_thumbnail": "áēĸnh thu nháģ", "cache_settings_statistics_title": "MáģŠc sáģ­ dáģĨng báģ™ nháģ› Ä‘áģ‡m", - "cache_settings_subtitle": "Kiáģƒm soÃĄt hành vi báģ™ nháģ› Ä‘áģ‡m cáģ§a app Immich", + "cache_settings_subtitle": "Kiáģƒm soÃĄt hành vi báģ™ nháģ› Ä‘áģ‡m cáģ§a Immich", "cache_settings_tile_subtitle": "Kiáģƒm soÃĄt cÃĄch xáģ­ lÃŊ lưu tráģ¯ cáģĨc báģ™", "cache_settings_tile_title": "Lưu tráģ¯ cáģĨc báģ™", "cache_settings_title": "Cài đáēˇt báģ™ nháģ› Ä‘áģ‡m", @@ -871,8 +871,8 @@ "current_pin_code": "MÃŖ PIN hiáģ‡n táēĄi", "current_server_address": "Đáģ‹a cháģ§ mÃĄy cháģ§ hiáģ‡n táēĄi", "custom_date": "Thiáēŋt láē­p ngày tÚy cháģ‰nh", - "custom_locale": "Ngôn ngáģ¯ và khu váģąc", - "custom_locale_description": "Đáģ‹nh dáēĄng ngày và sáģ‘ dáģąa trÃĒn ngôn ngáģ¯ và khu váģąc", + "custom_locale": "Khu váģąc tÚy cháģ‰nh", + "custom_locale_description": "Đáģ‹nh dáēĄng ngày, tháģi gian và sáģ‘ dáģąa trÃĒn ngôn ngáģ¯ và khu váģąc Ä‘ÃŖ cháģn", "custom_url": "URL tÚy cháģ‰nh", "cutoff_date_description": "Giáģ¯ láēĄi áēŖnh trong vÃ˛ngâ€Ļ", "cutoff_day": "{count, plural, one {ngày} other {ngày}}", @@ -880,7 +880,7 @@ "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Táģ‘i", - "dark_theme": "Đáģ•i giao diáģ‡n", + "dark_theme": "Chuyáģƒn sang cháģ§ Ä‘áģ táģ‘i", "date": "Ngày", "date_after": "Ngày sau", "date_and_time": "Ngày và giáģ", @@ -891,10 +891,6 @@ "day": "Ngày", "days": "Ngày", "deduplicate_all": "XÃŗa táēĨt cáēŖ máģĨc trÚng láēˇp", - "deduplication_criteria_1": "Kích cáģĄ áēŖnh theo byte", - "deduplication_criteria_2": "Sáģ‘ lưáģŖng dáģ¯ liáģ‡u EXIF", - "deduplication_info": "Thông tin loáēĄi báģ dáģ¯ liáģ‡u trÚng láēˇp", - "deduplication_info_description": "Đáģƒ táģą Ä‘áģ™ng cháģn trưáģ›c và loáēĄi báģ cÃĄc táģ‡p trÚng láēˇp hàng loáēĄt, chÃēng tôi sáēŊ xem xÊt dáģąa trÃĒn:", "delete": "XÃŗa", "delete_action_confirmation_message": "BáēĄn cÃŗ cháē¯c muáģ‘n xÃŗa táģ‡p này? Thao tÃĄc này sáēŊ chuyáģƒn táģ‡p vào thÚng rÃĄc cáģ§a mÃĄy cháģ§ và sáēŊ háģi báēĄn cÃŗ muáģ‘n xÃŗa nÃŗ cáģĨc báģ™ không", "delete_action_prompt": "{count} Ä‘ÃŖ xÃŗa", @@ -1003,8 +999,8 @@ "editor_discard_edits_confirm": "Báģ thay đáģ•i", "editor_discard_edits_prompt": "BáēĄn cÃŗ nháģ¯ng thay đáģ•i chưa đưáģŖc lưu. BáēĄn cÃŗ cháē¯c cháē¯n muáģ‘n háģ§y báģ chÃēng không?", "editor_discard_edits_title": "Háģ§y thay đáģ•i?", - "editor_edits_applied_error": "Láģ—i khi ÃĄp dáģĨng thay đáģ•i", - "editor_edits_applied_success": "Thay đáģ•i đưáģŖc ÃĄp dáģĨng thành công", + "editor_edits_applied_error": "Không tháģƒ ÃĄp dáģĨng cháģ‰nh sáģ­a", + "editor_edits_applied_success": "Cháģ‰nh sáģ­a đưáģŖc ÃĄp dáģĨng thành công", "editor_flip_horizontal": "Láē­t ngang", "editor_flip_vertical": "Láē­t dáģc", "editor_orientation": "Đáģ‹nh hưáģ›ng", @@ -1072,7 +1068,7 @@ "failed_to_update_notification_status": "Cáē­p nháē­t tráēĄng thÃĄi thông bÃĄo tháēĨt báēĄi", "incorrect_email_or_password": "Email hoáēˇc máē­t kháēŠu không chính xÃĄc", "library_folder_already_exists": "Đưáģng dáēĢn nháē­p này Ä‘ÃŖ táģ“n táēĄi.", - "page_not_found": "Không tÃŦm tháēĨy trang :/", + "page_not_found": "Không tÃŦm tháēĨy trang", "paths_validation_failed": "{paths, plural, one {# đưáģng dáēĢn} other {# đưáģng dáēĢn}} không háģŖp láģ‡", "profile_picture_transparent_pixels": "áēĸnh đáēĄi diáģ‡n không tháģƒ cÃŗ điáģƒm áēŖnh trong suáģ‘t. Vui lÃ˛ng phÃŗng to và/hoáēˇc di chuyáģƒn hÃŦnh áēŖnh.", "quota_higher_than_disk_size": "BáēĄn Ä‘ÃŖ đáēˇt háēĄn máģŠc cao hÆĄn dung lưáģŖng áģ• Ä‘ÄŠa", @@ -1206,7 +1202,7 @@ "feature_photo_updated": "ÄÃŖ cáē­p nháē­t áēŖnh náģ•i báē­t", "features": "Tính năng", "features_in_development": "Tính năng đang đưáģŖc phÃĄt triáģƒn", - "features_setting_description": "QuáēŖn lÃŊ cÃĄc tính năng app", + "features_setting_description": "QuáēŖn lÃŊ cÃĄc tính năng áģŠng dáģĨng", "file_name_or_extension": "TÃĒn hoáēˇc pháē§n máģŸ ráģ™ng táē­p tin", "file_name_text": "TÃĒn táģ‡p", "file_name_with_value": "TÃĒn táģ‡p: {file_name}", @@ -1284,7 +1280,7 @@ "home_page_delete_remote_err_local": "Táģ‡p trÃĒn thiáēŋt báģ‹ trong láģąa cháģn xÃŗa táģĢ xa, báģ qua", "home_page_favorite_err_local": "Không tháģƒ thích táģ‡p trÃĒn thiáēŋt báģ‹, báģ qua", "home_page_favorite_err_partner": "Không tháģƒ thích táģ‡p cáģ§a ngưáģi thÃĸn, báģ qua", - "home_page_first_time_notice": "Náēŋu đÃĸy là láē§n đáē§u báēĄn dÚng app, hÃŖy cháģn máģ™t album sao lưu đáģƒ dÃ˛ng tháģi gian cÃŗ tháģƒ hiáģƒn tháģ‹ áēŖnh và video cáģ§a báēĄn", + "home_page_first_time_notice": "Náēŋu đÃĸy là láē§n đáē§u báēĄn dÚng áģŠng dáģĨng, hÃŖy cháģn máģ™t album sao lưu đáģƒ dÃ˛ng tháģi gian cÃŗ tháģƒ hiáģƒn tháģ‹ áēŖnh và video cáģ§a báēĄn", "home_page_locked_error_local": "Không tháģƒ di chuyáģƒn táģ‡p trÃĒn thiáēŋt báģ‹ Ä‘áēŋn thư máģĨc KhÃŗa, báģ qua", "home_page_locked_error_partner": "Không tháģƒ di chuyáģƒn táģ‡p cáģ§a ngưáģi thÃĸn đáēŋn thư máģĨc KhÃŗa, báģ qua", "home_page_share_err_local": "Không tháģƒ chia sáēģ táģ‡p trÃĒn thiáēŋt báģ‹ qua liÃĒn káēŋt, báģ qua", @@ -1399,7 +1395,7 @@ "local_id": "ID cáģĨc báģ™", "local_media_summary": "Mô táēŖ phÆ°ÆĄng tiáģ‡n trÃĒn thiáēŋt báģ‹", "local_network": "MáēĄng náģ™i báģ™", - "local_network_sheet_info": "App sáēŊ káēŋt náģ‘i váģ›i mÃĄy cháģ§ qua URL này khi sáģ­ dáģĨng máēĄng Wi-Fi đưáģŖc cháģ‰ Ä‘áģ‹nh", + "local_network_sheet_info": "áģ¨ng dáģĨng sáēŊ káēŋt náģ‘i váģ›i mÃĄy cháģ§ qua URL này khi sáģ­ dáģĨng máēĄng Wi-Fi đưáģŖc cháģ‰ Ä‘áģ‹nh", "location": "Đáģ‹a điáģƒm", "location_permission": "Quyáģn truy cáē­p váģ‹ trí", "location_permission_content": "Đáģƒ sáģ­ dáģĨng tính năng táģą Ä‘áģ™ng chuyáģƒn đáģ•i, Immich cáē§n cÃŗ quyáģn váģ‹ trí chính xÃĄc đáģƒ cÃŗ tháģƒ Ä‘áģc tÃĒn cáģ§a máēĄng Wi-Fi hiáģ‡n táēĄi", @@ -1475,11 +1471,11 @@ "manage_geolocation": "QuáēŖn lÃŊ đáģ‹a điáģƒm", "manage_media_access_rationale": "Đáģƒ cÃŗ tháģƒ di chuyáģƒn táģ‡p vào thÚng rÃĄc và khôi pháģĨc chÃēng táģĢ Ä‘Ãŗ.", "manage_media_access_settings": "MáģŸ cài đáēˇt", - "manage_media_access_subtitle": "Cho phÊp app [Immich] quáēŖn lÃŊ và di chuyáģƒn táģ‡p.", + "manage_media_access_subtitle": "Cho phÊp áģŠng dáģĨng [Immich] quáēŖn lÃŊ và di chuyáģƒn táģ‡p.", "manage_media_access_title": "QuáēŖn lÃŊ phÆ°ÆĄng tiáģ‡n", "manage_shared_links": "QuáēŖn lÃŊ liÃĒn káēŋt chia sáēģ", "manage_sharing_with_partners": "QuáēŖn lÃŊ chia sáēģ váģ›i ngưáģi thÃĸn", - "manage_the_app_settings": "QuáēŖn lÃŊ cài đáēˇt app", + "manage_the_app_settings": "QuáēŖn lÃŊ cài đáēˇt áģŠng dáģĨng", "manage_your_account": "QuáēŖn lÃŊ tài khoáēŖn cáģ§a báēĄn", "manage_your_api_keys": "QuáēŖn lÃŊ cÃĄc khÃŗa API cáģ§a báēĄn", "manage_your_devices": "QuáēŖn lÃŊ cÃĄc thiáēŋt báģ‹ Ä‘ÃŖ đăng nháē­p cáģ§a báēĄn", @@ -1494,7 +1490,7 @@ "map_marker_for_images": "ÄÃĄnh dáēĨu báēŖn đáģ“ cho áēŖnh cháģĨp táēĄi {city}, {country}", "map_marker_with_image": "ÄÃĄnh dáēĨu báēŖn đáģ“ váģ›i áēŖnh", "map_no_location_permission_content": "Cáē§n quyáģn truy cáē­p váģ‹ trí đáģƒ hiáģƒn tháģ‹ táģ‡p táģĢ váģ‹ trí hiáģ‡n táēĄi cáģ§a báēĄn. BáēĄn cÃŗ muáģ‘n cho phÊp ngay bÃĸy giáģ không?", - "map_no_location_permission_title": "App không đưáģŖc phÊp truy cáē­p váģ‹ trí", + "map_no_location_permission_title": "áģ¨ng dáģĨng không đưáģŖc phÊp truy cáē­p váģ‹ trí", "map_settings": "Cài đáēˇt báēŖn đáģ“", "map_settings_dark_mode": "Cháēŋ đáģ™ táģ‘i", "map_settings_date_range_option_day": "Trong vÃ˛ng 24 giáģ qua", @@ -1649,6 +1645,7 @@ "only_favorites": "Cháģ‰ lưáģŖt thích", "open": "MáģŸ", "open_calendar": "Hiáģ‡n tháģ‹ láģ‹ch", + "open_in_browser": "MáģŸ trong trÃŦnh duyáģ‡t", "open_in_map_view": "MáģŸ trong báēŖn đáģ“", "open_in_openstreetmap": "MáģŸ trong OpenStreetMap", "open_the_search_filters": "MáģŸ báģ™ láģc tÃŦm kiáēŋm", @@ -1749,7 +1746,7 @@ "play_transcoded_video": "PhÃĄt video Ä‘ÃŖ chuyáģƒn mÃŖ", "please_auth_to_access": "Vui lÃ˛ng xÃĄc tháģąc đáģƒ truy cáē­p", "port": "Cáģ•ng", - "preferences_settings_subtitle": "TÚy cháģ‰nh tráēŖi nghiáģ‡m app", + "preferences_settings_subtitle": "TÚy cháģ‰nh tráēŖi nghiáģ‡m áģŠng dáģĨng", "preferences_settings_title": "CÃĄ nhÃĸn hÃŗa", "preparing": "Đang chuáēŠn báģ‹", "preset": "MáēĢu cÃŗ sáēĩn", @@ -1763,7 +1760,7 @@ "primary": "Chính", "privacy": "BáēŖo máē­t", "profile": "Háģ“ sÆĄ", - "profile_drawer_app_logs": "Log", + "profile_drawer_app_logs": "Nháē­t kÃŊ", "profile_drawer_client_server_up_to_date": "MÃĄy khÃĄch và mÃĄy cháģ§ Ä‘ÃŖ cáē­p nháē­t", "profile_drawer_github": "GitHub", "profile_drawer_readonly_mode": "ÄÃŖ báē­t cháēŋ đáģ™ cháģ‰-xem. NháēĨn giáģ¯ áēŖnh đáēĄi diáģ‡n ngưáģi dÚng đáģƒ táē¯t.", @@ -1808,7 +1805,7 @@ "rate_asset": "Asset ÄÃĄnh giÃĄ", "rating": "Xáēŋp háēĄng sao", "rating_clear": "XÃŗa xáēŋp háēĄng", - "rating_count": "{count, plural, one {# sao} other {# sao}}", + "rating_count": "{count, plural, =0 {Chưa xáēŋp háēĄng} one {# sao} other {# sao}}", "rating_description": "Hiáģƒn tháģ‹ xáēŋp háēĄng EXIF trong báēŖng thông tin", "reaction_options": "TÚy cháģn pháēŖn áģŠng", "read_changelog": "Đáģc nháē­t kÃŊ thay đáģ•i", @@ -1881,7 +1878,10 @@ "reset_pin_code_success": "Đáēˇt láēĄi mÃŖ PIN thành công", "reset_pin_code_with_password": "BáēĄn luôn cÃŗ tháģƒ Ä‘áēˇt láēĄi mÃŖ PIN cáģ§a báēĄn báēąng máē­t kháēŠu cáģ§a báēĄn", "reset_sqlite": "Đáēˇt láēĄi cÆĄ sáģŸ dáģ¯ liáģ‡u SQLite", - "reset_sqlite_confirmation": "BáēĄn cÃŗ cháē¯c muáģ‘n đáēˇt láēĄi cÆĄ sáģŸ dáģ¯ liáģ‡u SQLite? BáēĄn sáēŊ cáē§n đăng xuáēĨt và đăng nháē­p láēĄi đáģƒ Ä‘áģ“ng báģ™ láēĄi dáģ¯ liáģ‡u", + "reset_sqlite_clear_app_data": "XÃŗa dáģ¯ liáģ‡u", + "reset_sqlite_confirmation": "BáēĄn cÃŗ cháē¯c muáģ‘n xÃŗa dáģ¯ liáģ‡u áģŠng dáģĨng không? Thao tÃĄc này sáēŊ xÃŗa táēĨt cáēŖ cài đáēˇt và đăng xuáēĨt báēĄn kháģi áģŠng dáģĨng.", + "reset_sqlite_confirmation_note": "Lưu ÃŊ: BáēĄn cáē§n kháģŸi đáģ™ng láēĄi áģŠng dáģĨng sau khi xÃŗa.", + "reset_sqlite_done": "Dáģ¯ liáģ‡u áģŠng dáģĨng Ä‘ÃŖ đưáģŖc xÃŗa. Vui lÃ˛ng kháģŸi đáģ™ng láēĄi Immich và đăng nháē­p láēĄi.", "reset_sqlite_success": "ÄÃŖ thiáēŋt láē­p láēĄi cÆĄ sáģŸ dáģ¯ liáģ‡u SQLite thành công", "reset_to_default": "Đáēˇt láēĄi váģ máēˇc đáģ‹nh", "resolution": "Đáģ™ phÃĸn giáēŖi", @@ -2006,7 +2006,7 @@ "send_message": "Gáģ­i tin nháē¯n", "send_welcome_email": "Gáģ­i email chào máģĢng", "server_endpoint": "Đáģ‹a cháģ‰ mÃĄy cháģ§", - "server_info_box_app_version": "PhiÃĒn báēŖn app", + "server_info_box_app_version": "PhiÃĒn báēŖn áģŠng dáģĨng", "server_info_box_server_url": "URL mÃĄy cháģ§", "server_offline": "MÃĄy cháģ§ ngoáēĄi tuyáēŋn", "server_online": "PhiÃĒn báēŖn", @@ -2228,7 +2228,7 @@ "theme_setting_primary_color_title": "Màu cháģ§ Ä‘áēĄo", "theme_setting_system_primary_color_title": "DÚng màu háģ‡ tháģ‘ng", "theme_setting_system_theme_switch": "Táģą Ä‘áģ™ng (Giáģ‘ng thiáēŋt báģ‹)", - "theme_setting_theme_subtitle": "Cháģn cài đáēˇt giao diáģ‡n app", + "theme_setting_theme_subtitle": "Cháģn cài đáēˇt giao diáģ‡n áģŠng dáģĨng", "theme_setting_three_stage_loading_subtitle": "TáēŖi ba giai đoáēĄn cÃŗ tháģƒ tăng táģ‘c đáģ™ táēŖi áēŖnh nhưng sáēŊ táģ‘n dáģ¯ liáģ‡u máēĄng Ä‘ÃĄng káģƒ", "theme_setting_three_stage_loading_title": "Báē­t táēŖi ba giai đoáēĄn", "then": "Tiáēŋp theo", @@ -2276,7 +2276,7 @@ "troubleshoot": "Kháē¯c pháģĨc sáģą cáģ‘", "type": "LoáēĄi", "unable_to_change_pin_code": "Thay đáģ•i mÃŖ PIN tháēĨt báēĄi", - "unable_to_check_version": "Không tháģƒ kiáģƒm tra phiÃĒn báēŖn app hoáēˇc mÃĄy cháģ§", + "unable_to_check_version": "Không tháģƒ kiáģƒm tra phiÃĒn báēŖn áģŠng dáģĨng hoáēˇc mÃĄy cháģ§", "unable_to_setup_pin_code": "Thiáēŋt láē­p mÃŖ PIN tháēĨt báēĄi", "unarchive": "Báģ lưu tráģ¯", "unarchive_action_prompt": "{count} Ä‘ÃŖ báģ kháģi Lưu tráģ¯", @@ -2332,6 +2332,8 @@ "url": "URL", "usage": "Sáģ­ dáģĨng", "use_biometric": "DÚng sinh tráē¯c háģc", + "use_browser_locale": "DÚng ngôn ngáģ¯ trÃŦnh duyáģ‡t", + "use_browser_locale_description": "Đáģ‹nh dáēĄng ngày, tháģi gian và sáģ‘ dáģąa trÃĒn ngôn ngáģ¯ trÃŦnh duyáģ‡t", "use_current_connection": "DÚng káēŋt náģ‘i hiáģ‡n táēĄi", "use_custom_date_range": "Cháģn khoáēŖng tháģi gian tÚy cháģ‰nh", "user": "Ngưáģi dÚng", diff --git a/i18n/yue_Hant.json b/i18n/yue_Hant.json index ab1ff60fef..19666b0af5 100644 --- a/i18n/yue_Hant.json +++ b/i18n/yue_Hant.json @@ -86,18 +86,21 @@ "export_config_as_json_description": "å°‡į›Žå‰å˜…įŗģįĩąč¨­åŽšä¸‹čŧ‰į‚ē JSON æĒ”æĄˆ", "external_libraries_page_description": "įŽĄį†å¤–éƒ¨åĒ’éĢ”åēĢ嘅頁éĸ", "face_detection": "äēēéĸåĩæ¸Ŧ", - "face_detection_description": "į”¨æŠŸå™¨å­¸įŋ’åšŸæœå°‹į›¸ä¸­å˜…ã€‚", + "face_detection_description": "äŊŋį”¨æŠŸå™¨å­¸įŋ’åĩæ¸Ŧé …į›Žä¸­å˜…č‡‰å­”ã€‚å°æ–ŧåŊąį‰‡īŧŒåĒäŋ‚æœƒåˆ†æžį¸Žåœ–ã€‚ã€Œé‡æ–°æ•´į†ã€æœƒé‡æ–°č™•į†æ‰€æœ‰å˜…é …į›Žīŧ›ã€Œé‡č¨­ã€å°ąæœƒéĄå¤–æ¸…é™¤į›Žå‰å˜…č‡‰å­”čŗ‡æ–™īŧ›ã€ŒåŠ å…ĨæŽ’į¨‹ã€æœƒå°‡å°šæœĒč™•į†å˜…é …į›ŽåŠ å…Ĩåēåˆ—ã€‚åŽŒæˆã€Œč‡‰å­”åĩæ¸Ŧ」垌īŧŒåĩæ¸Ŧåˆ°å˜…č‡‰å­”å°‡æœƒåŠ å…Ĩã€Œč‡‰å­”čž¨č­˜ã€æŽ’į¨‹īŧŒä¸Ļæ­¸éĄžåˆ°č€ŒåŽļæˆ–č€…æ–°å˜…äēēį‰Šįž¤įĩ„。", + "facial_recognition_job_description": "將åĩæ¸Ŧåˆ°å˜…č‡‰å­”æ­¸éĄžį‚ēäēēį‰Šã€‚æ­¤æ­ĨéŠŸæœƒåœ¨č‡‰å­”åĩæ¸ŦåŽŒæˆåžŒåŸˇčĄŒã€‚ã€Œé‡č¨­ã€æœƒé‡æ–°å°æ‰€æœ‰å˜…č‡‰å­”é€˛čĄŒåˆ†įž¤īŧ›ã€ŒåŠ å…ĨæŽ’į¨‹ã€å°ąæœƒå°‡æœĒ指洞äēēį‰Šå˜…č‡‰å­”åŠ å…Ĩåēåˆ—。", "failed_job_command": "åŸˇčĄŒ{job}äģģ務嘅{command}指äģ¤å¤ąæ•—", "force_delete_user_warning": "č­Ļ告īŧšå‘ĸ個會įĢ‹åŗåˆĒ除ᔍæˆļ同埋äŊĸ所有嘅æĒ”æĄˆã€‚å‘ĸ個äŋ‚į„Ąæŗ•æ’¤éŠˇå˜…å‹•äŊœīŧŒč€Œä¸”åˆĒ除嘅æĒ”æĄˆå°‡å†‡čžĻæŗ•åžŠåŽŸã€‚", "image_format": "æ ŧåŧ", "image_format_description": "WebP æ ŧåŧį›¸å˜…æĒ”æĄˆæœƒæ¯” JPEG į´°īŧŒäŊ†äŋ‚ᎍįĸŧ嘅速åēĻæœƒæ…ĸå•˛ã€‚", "image_fullsize_description": "厞åˆĒ除元數據嘅全å°ē坏ᛏīŧŒå–ēæ”žå¤§į›¸å˜…æ™‚å€™į”¨å˜…", "image_fullsize_enabled": "å•Ÿį”¨å…¨å°ēå¯¸å˜…åœ–į‰‡į”Ÿæˆ", - "image_fullsize_enabled_description": "į‚ē非įļ˛é å‹å–„æ ŧåŧį”Ÿæˆå¤§å°ēå¯¸åœ–į‰‡ã€‚å•Ÿį”¨", + "image_fullsize_enabled_description": "į‚ē非įļ˛é å‹å–„æ ŧåŧį”ĸį”Ÿå¤§å°ēå¯¸å˜…į›¸ã€‚å•Ÿį”¨ã€ŒååĨŊ內åĩŒé čĻŊ」嘅時候īŧŒįŗģįĩąå°‡æœƒį›´æŽĨᔍ內åĩŒå˜…預čĻŊč€Œå””é€˛čĄŒčŊ‰įĸŧ。å‘ĸå€‹č¨­åŽšå””æœƒåŊąéŸŋ JPEG į­‰įļ˛é å‹å–„æ ŧåŧã€‚", "image_fullsize_quality_description": "į”ą 1 到 100īŧŒį”Ÿæˆå…¨å°ēå¯¸åœ–į‰‡å˜…čŗĒį´ ã€‚æ•¸å€ŧčļŠé̘į•ĢčŗĒčļŠåĨŊīŧŒäŊ†äŋ‚æĒ”æĄˆæœƒæ›´åŠ å¤§ã€‚", "image_fullsize_title": "全å°ēå¯¸åœ–į‰‡č¨­åŽš", "image_prefer_embedded_preview": "偏向åĩŒå…Ĩ預čĻŊ", + "image_prefer_embedded_preview_setting_description": "å–ēå¯į”¨å˜…æ™‚å€™å°‡ RAW į›¸į‰‡ä¸­įš„å…§åĩŒé čĻŊäŊœį‚ēåŊąåƒč™•į†å˜…čŧ¸å…Ĩ來æēã€‚é›–į„ļå‘ĸå€‹č¨­åŽšå¯äģĨäģ¤åˆ°éƒ¨åˆ†į›¸į‰‡å˜…色åŊŠæ›´åŠ æē–įĸēīŧŒäŊ†é čĻŊ品čŗĒ取æąēæ–ŧį›¸æŠŸīŧŒä¸”åŊąåƒå¯čƒŊ會å‡ēįžčŧƒå¤šåŖ“į¸Žį‘•į–ĩ。", "image_prefer_wide_gamut": "傞向åģŖč‰˛åŸŸ", + "image_prefer_wide_gamut_setting_description": "äŊŋᔍ Display P3 čŖŊäŊœį¸Žåœ–īŧšå¯äģĨ更åĨŊ地äŋį•™åģŖč‰˛åŸŸåŊąåƒå˜…鎎蹔åēĻīŧŒäŊ†äŋ‚å–ēčˆŠčŖįŊŽåŒčˆŠį‰ˆį€čĻŊ器上īŧŒåŊąåƒå‘ˆįžå˜…效果可čƒŊ會有所唔同。sRGB åŊąåƒæœƒäŋį•™į‚ē sRGBīŧŒäģĨéŋå…č‰˛åŊŠåį§ģ。", "image_preview_description": "䏭ᭉå°ēå¯¸å˜…åœ–į‰‡īŧŒį”¨åšŸæĒĸčĻ–å–Žä¸€åŊąåƒåŒåŸ‹æŠŸå™¨å­¸įŋ’", "image_preview_title": "預čĻŊč¨­åŽš", "image_progressive": "逐æ­Ĩ", diff --git a/i18n/zh_Hans.json b/i18n/zh_Hans.json index 2a85e2e3b7..49dbce4035 100644 --- a/i18n/zh_Hans.json +++ b/i18n/zh_Hans.json @@ -5,7 +5,7 @@ "acknowledge": "厞įŸĨ悉", "action": "操äŊœ", "action_common_update": "更新", - "action_description": "å¯šį­›é€‰å‡ēįš„čĩ„äē§æ‰§čĄŒįš„一į섿“äŊœ", + "action_description": "å¯šį­›é€‰å‡ēįš„į…§į‰‡/视éĸ‘æ‰§čĄŒįš„ä¸€į섿“äŊœ", "actions": "操äŊœ", "active": "čŋ›čĄŒä¸­", "active_count": "æ´ģ动: {count}", @@ -18,7 +18,7 @@ "add_a_title": "æˇģ加标éĸ˜", "add_action": "æˇģ加操äŊœ", "add_action_description": "į‚šå‡ģäģĨæˇģ加čĻæ‰§čĄŒįš„æ“äŊœ", - "add_assets": "æˇģ加čĩ„äē§", + "add_assets": "æˇģåŠ éĄšį›Ž", "add_birthday": "æˇģåŠ į”Ÿæ—Ĩ", "add_endpoint": "æˇģ加įĢ¯į‚š", "add_exclusion_pattern": "æˇģåŠ æŽ’é™¤č§„åˆ™", @@ -34,13 +34,13 @@ "add_to_album": "æˇģåŠ åˆ°į›¸å†Œ", "add_to_album_bottom_sheet_added": "厞æˇģåŠ č‡ŗ {album}", "add_to_album_bottom_sheet_already_exists": "厞圍 {album} 中", - "add_to_album_bottom_sheet_some_local_assets": "部分æœŦ地čĩ„äē§æ— æŗ•æˇģåŠ åˆ°į›¸å†Œ", + "add_to_album_bottom_sheet_some_local_assets": "部分æœŦ地čĩ„äē§æ— æŗ•æˇģåŠ č‡ŗį›¸å†Œ", "add_to_album_toggle": "切æĸ {album} įš„é€‰ä¸­įŠļ态", "add_to_albums": "æˇģåŠ åˆ°į›¸å†Œ", "add_to_albums_count": "æˇģåŠ åˆ°į›¸å†Œ ({count})", "add_to_bottom_bar": "æˇģ加到", "add_to_shared_album": "æˇģåŠ åˆ°å…ąäēĢį›¸å†Œ", - "add_upload_to_stack": "æˇģ加上äŧ č‡ŗå †æ ˆ", + "add_upload_to_stack": "æˇģ加上äŧ č‡ŗå †å ", "add_url": "æˇģ加 URL", "add_workflow_step": "æˇģ加åˇĨäŊœæĩæ­ĨéǤ", "added_to_archive": "æˇģåŠ č‡ŗå­˜æĄŖ", @@ -49,9 +49,9 @@ "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_description": "įŽĄį†å¯†į ã€OAuth 和å…ļäģ–čŽ¤č¯čŽžįŊŽ", "authentication_settings_disable_all": "æ‚¨įĄŽåŽščρįĻį”¨æ‰€æœ‰į™ģåŊ•æ–šåŧå—īŧŸį™ģåŊ•功čƒŊå°†åŽŒå…¨å¤ąæ•ˆã€‚", "authentication_settings_reenable": "åĻ‚éœ€é‡æ–°å¯į”¨īŧŒč¯ˇäŊŋᔍ æœåŠĄå™¨å‘Ŋäģ¤ã€‚", "background_task_job": "后台äģģåŠĄ", @@ -61,40 +61,40 @@ "backup_onboarding_1_description": "åŧ‚地备äģŊīŧŒäž‹åĻ‚å­˜å‚¨åœ¨äē‘įĢ¯æˆ–åĻ一ä¸Ēį‰Šį†äŊįŊŽã€‚", "backup_onboarding_2_description": "æœŦåœ°å¤ščŽžå¤‡å‰¯æœŦã€‚åŗåœ¨ä¸åŒčŽžå¤‡ä¸Šäŋå­˜ä¸ģ文äģļ及å…ļæœŦ地备äģŊ。", "backup_onboarding_3_description": "æ•°æŽįš„æ€ģ副æœŦ数īŧŒåŒ…åĢ原始文äģļ。䞋åĻ‚īŧš1 äģŊåŧ‚地备äģŊ和 2 äģŊæœŦ地副æœŦ。", - "backup_onboarding_description": "åģēčŽŽé‡‡į”¨ 3-2-1 备äģŊį­–į•Ĩ æĨäŋæŠ¤æ‚¨įš„æ•°æŽã€‚äŊ åē”č¯Ĩäŋį•™åˇ˛ä¸Šäŧ įš„ᅧቇ/视éĸ‘äģĨ及 Immich 数捎åē“įš„å‰¯æœŦīŧŒäģĨåŽžįŽ°å…¨éĸįš„å¤‡äģŊč§Ŗå†ŗæ–šæĄˆã€‚", + "backup_onboarding_description": "åģēčŽŽé‡‡į”¨ 3-2-1 备äģŊį­–į•Ĩ æĨäŋæŠ¤äŊ įš„æ•°æŽã€‚ä¸ēäē†åŽžįŽ°å…¨éĸįš„å¤‡äģŊæ–šæĄˆīŧŒäŊ åē”č¯Ĩ同æ—ļäŋå­˜åˇ˛ä¸Šäŧ įš„ᅧቇ/视éĸ‘副æœŦäģĨ及 Immich įš„æ•°æŽåē“。", "backup_onboarding_footer": "æœ‰å…ŗå¤‡äģŊ Immich įš„æ›´å¤šäŋĄæ¯īŧŒč¯ˇå‚阅 æ–‡æĄŖã€‚", - "backup_onboarding_parts_title": "3-2-1 备äģŊį­–į•Ĩ包æ‹Ŧīŧš", + "backup_onboarding_parts_title": "3-2-1备äģŊ原则包æ‹Ŧīŧš", "backup_onboarding_title": "备äģŊ", "backup_settings": "数捎åē“备äģŊ莞įŊŽ", "backup_settings_description": "įŽĄį†æ•°æŽåē“备äģŊ莞įŊŽã€‚", "cleared_jobs": "åˇ˛æ¸…é™¤ {job} įš„äģģåŠĄ", "config_set_by_file": "åŊ“前配įŊŽį”ąé…įŊŽæ–‡äģļčŽžåŽš", - "confirm_delete_library": "įĄŽåŽščĻåˆ é™¤čĩ„äē§åē“ \"{library}\" 吗īŧŸ", - "confirm_delete_library_assets": "įĄŽåŽščĻåˆ é™¤æ­¤čĩ„äē§åē“吗īŧŸæ­¤æ“äŊœå°†äģŽ Immich 中删除 {count, plural, one {# ä¸Ē兺联čĩ„äē§} other {全部 # ä¸Ē兺联čĩ„äē§}}īŧŒä¸”æ— æŗ•æ’¤é”€ã€‚æŗ¨æ„īŧšæ–‡äģļäģå°†äŋį•™åœ¨įŖį›˜ä¸Šã€‚", - "confirm_email_below": "ä¸ēįĄŽčŽ¤æ“äŊœīŧŒč¯ˇåœ¨ä¸‹æ–ščž“å…Ĩ \"{email}\"", + "confirm_delete_library": "įĄŽåŽščĻåˆ é™¤čĩ„æēåē“ \"{library}\" 吗īŧŸ", + "confirm_delete_library_assets": "įĄŽåŽščĻåˆ é™¤æ­¤čĩ„æēåē“吗īŧŸæ­¤æ“äŊœå°†äģŽ Immich 中删除 {count, plural, one {# ä¸Ēå…ŗč”éĄšį›Ž} other {å…ą # ä¸Ēå…ŗč”éĄšį›Ž}}īŧŒä¸”æ— æŗ•æ’¤é”€ã€‚æŗ¨æ„īŧšæ–‡äģļäģå°†äŋį•™åœ¨įŖį›˜ä¸Šã€‚", + "confirm_email_below": "ä¸ēįĄŽčŽ¤æ“äŊœīŧŒč¯ˇåœ¨ä¸‹æ–ščž“å…Ĩ“{email}”", "confirm_reprocess_all_faces": "įĄŽåŽščĻé‡æ–°å¤„į†æ‰€æœ‰äēē脏吗īŧŸæ­¤æ“äŊœå°†æ¸…除厞å‘Ŋåįš„äēēį‰Šã€‚", "confirm_user_password_reset": "įĄŽåŽščĻé‡įŊŽ {user} įš„å¯†į å—īŧŸ", "confirm_user_pin_code_reset": "įĄŽåŽščĻé‡įŊŽ {user} įš„ PIN ᠁吗īŧŸ", "copy_config_to_clipboard_description": "将åŊ“前įŗģįģŸé…įŊŽäŊœä¸ē JSON å¯ščąĄå¤åˆļ到å‰Ēč´´æŋ", "create_job": "创åģēäģģåŠĄ", - "cron_expression": "Cron 襨螞åŧ", - "cron_expression_description": "äŊŋᔍ Cron æ ŧåŧčŽžįŊŽæ‰Ģ描间隔。更多äŋĄæ¯č¯ˇå‚č€ƒ Crontab Guru į­‰įŊ‘įĢ™", - "cron_expression_presets": "Cron 襨螞åŧéĸ„莞", + "cron_expression": "Cron襨螞åŧ", + "cron_expression_description": "äŊŋᔍCronæ ŧåŧčŽžįŊŽæ‰Ģ描间隔。更多äŋĄæ¯č¯ˇå‚č€ƒ Crontab Guru į­‰įŊ‘įĢ™", + "cron_expression_presets": "Cron襨螞åŧéĸ„莞", "disable_login": "įρᔍį™ģåŊ•", "duplicate_detection_job_description": "čŋčĄŒæœē器å­Ļäš æĨæŖ€æĩ‹į›¸äŧŧ回像īŧŒæ­¤åŠŸčƒŊ䞝čĩ–äēŽæ™ēčƒŊ搜į´ĸ", - "exclusion_pattern_description": "æŽ’é™¤č§„åˆ™å…čŽ¸æ‚¨åœ¨æ‰Ģ描čĩ„äē§å瓿—ļåŋŊį•Ĩį‰šåŽšįš„æ–‡äģļ和文äģļ多。åĻ‚æžœæ‚¨æœ‰æŸäē›åŒ…åĢ不希望å¯ŧå…Ĩįš„æ–‡äģļīŧˆäž‹åĻ‚ RAW æ ŧåŧæ–‡äģļīŧ‰įš„æ–‡äģļ多īŧŒæ­¤åŠŸčƒŊå°†éžå¸¸æœ‰į”¨ã€‚", + "exclusion_pattern_description": "æŽ’é™¤č§„åˆ™å…čŽ¸æ‚¨åœ¨æ‰Ģ描čĩ„æēå瓿—ļåŋŊį•Ĩį‰šåŽšįš„æ–‡äģļ和文äģļ多。åĻ‚æžœæ‚¨æœ‰æŸäē›åŒ…åĢ不希望å¯ŧå…Ĩįš„æ–‡äģļīŧˆäž‹åĻ‚ RAW æ ŧåŧæ–‡äģļīŧ‰įš„æ–‡äģļ多īŧŒæ­¤åŠŸčƒŊå°†éžå¸¸æœ‰į”¨ã€‚", "export_config_as_json_description": "将åŊ“前įŗģįģŸé…įŊŽä¸‹čŊŊä¸ē JSON 文äģļ", - "external_libraries_page_description": "įŽĄį†å¤–éƒ¨čĩ„äē§åē“", + "external_libraries_page_description": "įŽĄį†å¤–éƒ¨čĩ„æēåē“", "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_format_description": "WebPæ ŧåŧįš„æ–‡äģļäŊ“į§¯æ¯”JPEG更小īŧŒäŊ†įŧ–į é€ŸåēĻ螃æ…ĸ。", "image_fullsize_description": "厞å‰ĨįĻģå…ƒæ•°æŽįš„å…¨å°ē寸回像īŧŒæ”žå¤§æŸĨįœ‹æ—ļäŊŋᔍ", "image_fullsize_enabled": "吝ᔍ免å°ēå¯¸å›žåƒį”Ÿæˆ", - "image_fullsize_enabled_description": "ä¸ē非įŊ‘éĄĩ友åĨŊæ ŧåŧį”Ÿæˆå…¨å°ēå¯¸å›žåƒã€‚å¯į”¨â€œäŧ˜å…ˆäŊŋᔍåĩŒå…Ĩåŧéĸ„č§ˆâ€åŽīŧŒå°†į›´æŽĨäŊŋᔍåĩŒå…Ĩåŧéĸ„č§ˆč€Œæ— éœ€čŊŦæĸã€‚æ­¤čŽžįŊŽä¸åŊąå“ JPEG į­‰įŊ‘éĄĩ友åĨŊæ ŧåŧã€‚", + "image_fullsize_enabled_description": "ä¸ē非įŊ‘éĄĩ友åĨŊæ ŧåŧį”Ÿæˆå…¨å°ēå¯¸å›žåƒã€‚å¯į”¨â€œäŧ˜å…ˆäŊŋᔍåĩŒå…Ĩåŧéĸ„č§ˆâ€åŽīŧŒå°†į›´æŽĨäŊŋᔍåĩŒå…Ĩåŧéĸ„č§ˆč€Œæ— éœ€čŊŦæĸã€‚æ­¤čŽžįŊŽä¸åŊąå“JPEGį­‰įŊ‘éĄĩ友åĨŊæ ŧåŧã€‚", "image_fullsize_quality_description": "全å°ēå¯¸å›žåƒč´¨é‡īŧˆ1-100īŧ‰ã€‚æ•°å€ŧčļŠé̘į”ģč´¨čļŠåĨŊīŧŒäŊ†į”Ÿæˆįš„æ–‡äģļ也čļŠå¤§ã€‚", "image_fullsize_title": "全å°ēå¯¸å›žåƒčŽžįŊŽ", "image_prefer_embedded_preview": "äŧ˜å…ˆäŊŋᔍåĩŒå…Ĩåŧéĸ„č§ˆ", @@ -115,29 +115,29 @@ "image_thumbnail_quality_description": "įŧŠį•Ĩå›žč´¨é‡īŧˆ1-100īŧ‰ã€‚æ•°å€ŧčļŠé̘į”ģč´¨čļŠåĨŊīŧŒäŊ†į”Ÿæˆįš„æ–‡äģļčļŠå¤§īŧŒä¸”可čƒŊ降äŊŽåē”į”¨å“åē”速åēĻ。", "image_thumbnail_title": "įŧŠį•Ĩå›žčŽžįŊŽ", "import_config_from_json_description": "通čŋ‡ä¸Šäŧ  JSON 配įŊŽæ–‡äģļå¯ŧå…ĨįŗģįģŸé…įŊŽ", - "job_concurrency": "{job} åšļ发数", + "job_concurrency": "{job}åšļ发数", "job_created": "äģģåŠĄåˇ˛åˆ›åģē", "job_not_concurrency_safe": "č¯ĨäģģåŠĄä¸æ”¯æŒåšļ发操äŊœã€‚", "job_settings": "äģģåŠĄčŽžįŊŽ", "job_settings_description": "įŽĄį†äģģåŠĄåšļ发数", - "jobs_delayed": "{jobCount, plural, other {# ä¸ĒåģļčŋŸ}}", + "jobs_delayed": "{jobCount, plural, other {#ä¸ĒåģļčŋŸ}}", "jobs_failed": "{jobCount, plural, other {# ä¸Ēå¤ąč´Ĩ}}", "jobs_over_time": "äģģåŠĄåŠ¨æ€", - "library_created": "åˇ˛åˆ›åģēčĩ„äē§åē“īŧš{library}", - "library_deleted": "čĩ„äē§åē“åˇ˛åˆ é™¤", - "library_details": "čĩ„äē§åē“č¯Ļ情", + "library_created": "åˇ˛åˆ›åģēčĩ„æēåē“īŧš{library}", + "library_deleted": "čĩ„æēåē“åˇ˛åˆ é™¤", + "library_details": "čĩ„æēåē“č¯Ļ情", "library_folder_description": "指厚一ä¸Ēå¯ŧå…Ĩ文äģļ多。įŗģįģŸå°†æ‰Ģ描č¯Ĩ文äģļ多及å…ļ所有子文äģļå¤šä¸­įš„å›žį‰‡å’Œč§†éĸ‘。", "library_remove_exclusion_pattern_prompt": "įĄŽåŽščρį§ģé™¤æ­¤æŽ’é™¤č§„åˆ™å—īŧŸ", "library_remove_folder_prompt": "įĄŽåŽščρį§ģ除此å¯ŧå…Ĩ文äģļ多吗īŧŸ", "library_scanning": "厚期æ‰Ģ描", "library_scanning_description": "配įŊŽåŽšæœŸæ‰Ģ描", "library_scanning_enable_description": "åŧ€å¯åŽšæœŸæ‰Ģ描", - "library_settings": "外部čĩ„äē§åē“", - "library_settings_description": "įŽĄį†å¤–éƒ¨čĩ„äē§åē“莞įŊŽ", - "library_tasks_description": "æ‰Ģ描外部čĩ„äē§åē“äģĨæŸĨ扞新åĸžå’Œå˜æ›´įš„æ–‡äģļ", - "library_updated": "čĩ„äē§åē“åˇ˛æ›´æ–°", - "library_watching_enable_description": "į›‘æŽ§å¤–éƒ¨čĩ„äē§åē“įš„æ–‡äģļ变更", - "library_watching_settings": "čĩ„äē§åē“į›‘æŽ§ [厞éĒŒæ€§åŠŸčƒŊ]", + "library_settings": "外部čĩ„æēåē“", + "library_settings_description": "įŽĄį†å¤–éƒ¨čĩ„æēåē“莞įŊŽ", + "library_tasks_description": "æ‰Ģ描外部čĩ„æēåē“äģĨæŸĨ扞新åĸžå’Œå˜æ›´įš„æ–‡äģļ", + "library_updated": "čĩ„æēåē“åˇ˛æ›´æ–°", + "library_watching_enable_description": "į›‘æŽ§å¤–éƒ¨čĩ„æēåē“įš„æ–‡äģļ变更", + "library_watching_settings": "čĩ„æēåē“į›‘æŽ§ [厞éĒŒæ€§åŠŸčƒŊ]", "library_watching_settings_description": "č‡ĒåŠ¨į›‘æŽ§æ–‡äģļ变更", "logging_enable_description": "吝ᔍæ—Ĩåŋ—čްåŊ•", "logging_level_description": "å¯į”¨åŽīŧŒæ‰€é‡‡į”¨įš„æ—Ĩåŋ—įē§åˆĢ。", @@ -149,11 +149,11 @@ "machine_learning_availability_checks_interval_description": "两æŦĄå¯į”¨æ€§æŖ€æŸĨäš‹é—´įš„æ—ļ间间隔īŧˆæ¯Ģį§’īŧ‰", "machine_learning_availability_checks_timeout": "č¯ˇæą‚čļ…æ—ļæ—ļ间", "machine_learning_availability_checks_timeout_description": "å¯į”¨æ€§æŖ€æŸĨįš„č¯ˇæą‚čļ…æ—ļæ—ļ间īŧˆæ¯Ģį§’īŧ‰", - "machine_learning_clip_model": "CLIP æ¨Ąåž‹", + "machine_learning_clip_model": "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": "č‹Ĩå…ŗé—­æ­¤å¤„æ€ģåŧ€å…ŗīŧŒæ‰€æœ‰æœē器å­Ļäš į›¸å…ŗį‰šæ€§å°†å…¨éƒ¨åœį”¨īŧŒä¸‹æ–šå…ˇäŊ“莞įŊŽæ— æ•ˆã€‚", @@ -181,12 +181,12 @@ "machine_learning_ocr_min_detection_score_description": "文æœŦæŖ€æĩ‹įš„æœ€äŊŽįŊŽäŋĄåēĻ分数īŧˆ0-1īŧ‰ã€‚æ•°å€ŧčļŠäŊŽīŧŒæŖ€æĩ‹åˆ°įš„æ–‡æœŦčļŠå¤šīŧŒäŊ†å¯čƒŊå‡ēįŽ°č¯¯åˆ¤ã€‚", "machine_learning_ocr_min_recognition_score": "最äŊŽč¯†åˆĢ阈å€ŧ", "machine_learning_ocr_min_score_recognition_description": "åˇ˛æŖ€æĩ‹æ–‡æœŦįš„æœ€äŊŽįŊŽäŋĄåēĻ分数īŧˆ0-1īŧ‰ã€‚æ•°å€ŧčļŠäŊŽīŧŒč¯†åˆĢå‡ēįš„æ–‡æœŦčļŠå¤šīŧŒäŊ†å¯čƒŊå‡ēįŽ°č¯¯åˆ¤ã€‚", - "machine_learning_ocr_model": "OCR æ¨Ąåž‹", + "machine_learning_ocr_model": "OCRæ¨Ąåž‹", "machine_learning_ocr_model_description": "æœåŠĄå™¨įĢ¯æ¨Ąåž‹æ¯”į§ģ动įĢ¯æ¨Ąåž‹æ›´į˛žå‡†īŧŒäŊ†å¤„ᐆ耗æ—ļ更é•ŋä¸”æ›´å į”¨å†…å­˜ã€‚", "machine_learning_settings": "æœē器å­Ļ䚠莞įŊŽ", "machine_learning_settings_description": "įŽĄį†æœē器å­Ļ䚠功čƒŊåŠį›¸å…ŗčŽžįŊŽ", "machine_learning_smart_search": "æ™ēčƒŊ搜į´ĸ", - "machine_learning_smart_search_description": "äŊŋᔍ CLIP åĩŒå…Ĩ向量čŋ›čĄŒč¯­äš‰åŒ–å›žį‰‡æœį´ĸ", + "machine_learning_smart_search_description": "äŊŋᔍCLIPåĩŒå…Ĩ向量čŋ›čĄŒč¯­äš‰åŒ–å›žį‰‡æœį´ĸ", "machine_learning_smart_search_enabled": "吝ᔍæ™ēčƒŊ搜į´ĸ", "machine_learning_smart_search_enabled_description": "č‹ĨįρᔍīŧŒå›žį‰‡å°†ä¸äŧščĸĢįŧ–᠁äģĨᔍäēŽæ™ēčƒŊ搜į´ĸ。", "machine_learning_url_description": "æœē器å­Ļäš æœåŠĄå™¨įš„ URL。č‹Ĩ提䞛多ä¸Ē URLīŧŒįŗģįģŸå°†æŒ‰äģŽå‰åž€åŽįš„éĄēåēé€ä¸Ēå°č¯•čŋžæŽĨīŧŒį›´č‡ŗæœ‰æœåŠĄå™¨æˆåŠŸå“åē”ä¸ēæ­ĸ。æœĒčƒŊ响åē”įš„æœåŠĄå™¨å°†čĸĢæš‚æ—ļåŋŊį•ĨīŧŒį›´č‡ŗå…￁ĸ复在įēŋ。", @@ -194,7 +194,7 @@ "maintenance_delete_backup_description": "此文äģļ将čĸĢæ°¸äš…删除。", "maintenance_delete_error": "删除备äģŊå¤ąč´Ĩ。", "maintenance_restore_backup": "æĸ复备äģŊ", - "maintenance_restore_backup_description": "Immich 数捎将čĸĢæ¸…除īŧŒåšļäģŽé€‰åŽšįš„å¤‡äģŊ中æĸ复。在įģ§įģ­äš‹å‰īŧŒå°†å…ˆåˆ›åģē一ä¸ĒåŊ“å‰æ•°æŽįš„å¤‡äģŊ。", + "maintenance_restore_backup_description": "Immich数捎将čĸĢæ¸…除īŧŒåšļäģŽé€‰åŽšįš„å¤‡äģŊ中æĸ复。在įģ§įģ­äš‹å‰īŧŒå°†å…ˆåˆ›åģē一ä¸ĒåŊ“å‰æ•°æŽįš„å¤‡äģŊ。", "maintenance_restore_backup_different_version": "此备äģŊæ˜¯į”ąä¸åŒį‰ˆæœŦįš„ Immich 创åģēįš„īŧ", "maintenance_restore_backup_unknown_version": "æ— æŗ•įĄŽåŽšå¤‡äģŊį‰ˆæœŦ。", "maintenance_restore_database_backup": "æĸ复数捎åē“备äģŊ", @@ -220,13 +220,13 @@ "map_reverse_geocoding_settings": "é€†åœ°į†įŧ–į čŽžįŊŽ", "map_settings": "地回", "map_settings_description": "įŽĄį†åœ°å›žčŽžįŊŽ", - "map_style_description": "style.json 地回ä¸ģéĸ˜įš„ URL", + "map_style_description": "style.json地回ä¸ģéĸ˜įš„URL", "memory_cleanup_job": "æ¸…į†å›žåŋ†æ•°æŽ", "memory_generate_job": "į”Ÿæˆå›žåŋ†", "metadata_extraction_job": "提取元数捎", - "metadata_extraction_job_description": "äģŽæ¯ä¸Ēčĩ„äē§ä¸­æå–元数捎äŋĄæ¯īŧŒäž‹åĻ‚ GPS、äēēč„¸å’Œåˆ†čž¨įŽ‡", + "metadata_extraction_job_description": "äģŽæ¯ä¸Ē文äģļ中提取元数捎äŋĄæ¯īŧŒäž‹åĻ‚GPS、äēēč„¸å’Œåˆ†čž¨įŽ‡", "metadata_faces_import_setting": "吝ᔍäēē脸å¯ŧå…Ĩ", - "metadata_faces_import_setting_description": "äģŽå›žį‰‡ EXIF 数捎和附å¸Ļ文äģļ中å¯ŧå…Ĩäēē脸äŋĄæ¯", + "metadata_faces_import_setting_description": "äģŽå›žį‰‡EXIF数捎和附å¸Ļ文äģļ中å¯ŧå…Ĩäēē脸äŋĄæ¯", "metadata_settings": "å…ƒæ•°æŽčŽžįŊŽ", "metadata_settings_description": "įŽĄį†å…ƒæ•°æŽčŽžįŊŽ", "migration_job": "čŋį§ģ", @@ -273,7 +273,7 @@ "oauth_auto_register_description": "į”¨æˆˇé€ščŋ‡ OAuth į™ģåŊ•后īŧŒč‡Ē动ä¸ēå…ļæŗ¨å†Œæ–°č´Ļæˆˇ", "oauth_button_text": "按钎文字", "oauth_client_secret_description": "æœē密åŽĸæˆˇį̝åŋ…åĄĢīŧŒæˆ–å…Ŧå…ąåŽĸæˆˇį̝č‹Ĩ不支持 PKCEīŧˆäģŖį ä礿ĸč¯æ˜Žå¯†é’Ĩīŧ‰æ—ļåŋ…åĄĢ。", - "oauth_enable_description": "äŊŋᔍ OAuth į™ģåŊ•", + "oauth_enable_description": "äŊŋᔍOAuthį™ģåŊ•", "oauth_mobile_redirect_uri": "į§ģ动įĢ¯é‡åŽšå‘ URI", "oauth_mobile_redirect_uri_override": "į§ģ动įĢ¯é‡åŽšå‘ URI čφᛖ", "oauth_mobile_redirect_uri_override_description": "åŊ“ OAuth æäž›å•†ä¸å…čŽ¸äŊŋᔍį§ģ动į̝ URIīŧˆäž‹åĻ‚ “{callback}”īŧ‰æ—ļ吝ᔍ", @@ -307,13 +307,13 @@ "require_password_change_on_login": "åŧēåˆļį”¨æˆˇéĻ–æŦĄį™ģåŊ•æ—ļäŋŽæ”šå¯†į ", "reset_settings_to_default": "å°†čŽžįŊŽé‡įŊŽä¸ēéģ˜čޤå€ŧ", "reset_settings_to_recent_saved": "å°†čŽžįŊŽé‡įŊŽä¸ē上æŦĄäŋå­˜įš„å€ŧ", - "scanning_library": "æ­Ŗåœ¨æ‰Ģ描čĩ„æ–™åē“", + "scanning_library": "æ­Ŗåœ¨æ‰Ģ描čĩ„æēåē“", "search_jobs": "搜į´ĸäģģåŠĄâ€Ļ", "send_welcome_email": "发送æŦĸčŋŽé‚Žäģļ", "server_external_domain_settings": "外部域名", "server_external_domain_settings_description": "å…Ŧåŧ€åˆ†äēĢ链æŽĨįš„åŸŸåīŧŒéœ€åŒ…åĢ http(s)://", "server_public_users": "į”¨æˆˇå…Ŧåŧ€", - "server_public_users_description": "åœ¨å°†į”¨æˆˇæˇģåŠ åˆ°å…ąäēĢį›¸å†Œæ—ļīŧŒæ‰€æœ‰į”¨æˆˇīŧˆå§“åå’Œé‚ŽįŽąīŧ‰éƒŊäŧščĸĢ列å‡ē。č‹Ĩå…ŗé—­æ­¤åŠŸčƒŊīŧŒį”¨æˆˇåˆ—čĄ¨å°†äģ…å¯šįŽĄį†å‘˜å¯č§ã€‚", + "server_public_users_description": "åœ¨å°†į”¨æˆˇæˇģåŠ č‡ŗå…ąäēĢį›¸å†Œæ—ļīŧŒäŧšåˆ—å‡ēæ‰€æœ‰į”¨æˆˇīŧˆåŒ…æ‹Ŧå§“åå’Œé‚ŽįŽąīŧ‰ã€‚č‹ĨįĻį”¨æ­¤é€‰éĄšīŧŒåˆ™äģ…įŽĄį†å‘˜å¯č§į”¨æˆˇåˆ—čĄ¨ã€‚", "server_settings": "æœåŠĄå™¨čŽžįŊŽ", "server_settings_description": "įŽĄį†æœåŠĄå™¨čŽžįŊŽ", "server_stats_page_description": "įŽĄį†æœåŠĄå™¨įģŸčŽĄéĄĩéĸ", @@ -323,21 +323,21 @@ "sidecar_job": "é™„åąžå…ƒæ•°æŽ", "sidecar_job_description": "äģŽæ–‡äģļįŗģįģŸä¸­å‘įŽ°æˆ–åŒæ­Ĩé™„åąžå…ƒæ•°æŽ", "slideshow_duration_description": "每åŧ å›žį‰‡æ˜žį¤ēįš„į§’æ•°", - "smart_search_job_description": "寚čĩ„äē§čŋčĄŒæœē器å­Ļäš äģĨ支持æ™ēčƒŊ搜į´ĸ", - "storage_template_date_time_description": "čĩ„äē§įš„创åģēæ—ļé—´æˆŗį”¨äēŽæ—Ĩ期æ—ļ间äŋĄæ¯", + "smart_search_job_description": "å¯šį…§į‰‡/视éĸ‘čŋčĄŒæœē器å­Ļäš äģĨ支持æ™ēčƒŊ搜į´ĸ", + "storage_template_date_time_description": "文äģļįš„åˆ›åģēæ—ļé—´æˆŗå°†į”¨äēŽæ—Ĩ期æ—ļ间äŋĄæ¯", "storage_template_date_time_sample": "į¤ē例æ—ļ间īŧš{date}", "storage_template_enable_description": "å¯į”¨å­˜å‚¨æ¨Ąæŋåŧ•擎", "storage_template_hash_verification_enabled": "å¯į”¨å“ˆå¸Œæ Ąénj", "storage_template_hash_verification_enabled_description": "åŧ€å¯å“ˆå¸Œæ Ąénj功čƒŊ。č‹Ĩ不清æĨšå…ŗé—­įš„后果īŧŒč¯ˇå‹ŋå…ŗé—­", "storage_template_migration": "å­˜å‚¨æ¨Ąæŋčŋį§ģ", - "storage_template_migration_description": "将åŊ“前 {template} åē”ᔍäēŽåˇ˛ä¸Šäŧ įš„čĩ„äē§", - "storage_template_migration_info": "å­˜å‚¨æ¨Ąæŋäŧšå°†æ‰€æœ‰æ–‡äģ￉Šåą•名čŊŦæĸä¸ēå°å†™ã€‚æ¨Ąæŋ更攚äģ…寚新上äŧ įš„čĩ„äē§į”Ÿæ•ˆã€‚č‹ĨčĻå°†æ¨Ąæŋ回æē¯åē”ᔍäēŽåˇ˛ä¸Šäŧ įš„čĩ„äē§īŧŒč¯ˇčŋčĄŒ {job}。", + "storage_template_migration_description": "将åŊ“前 {template} åē”ᔍäēŽåˇ˛ä¸Šäŧ įš„æ–‡äģļ", + "storage_template_migration_info": "å­˜å‚¨æ¨Ąæŋäŧšå°†æ‰€æœ‰æ–‡äģ￉Šåą•名čŊŦæĸä¸ēå°å†™ã€‚æ¨Ąæŋ更攚äģ…寚新上äŧ įš„æ–‡äģļį”Ÿæ•ˆã€‚č‹ĨčĻå°†æ¨Ąæŋ回æē¯åē”ᔍäēŽåˇ˛ä¸Šäŧ įš„æ–‡äģļīŧŒč¯ˇčŋčĄŒ {job}。", "storage_template_migration_job": "å­˜å‚¨æ¨Ąæŋčŋį§ģäģģåŠĄ", "storage_template_more_details": "æœ‰å…ŗæ­¤åŠŸčƒŊįš„æ›´å¤šč¯Ļįģ†äŋĄæ¯īŧŒč¯ˇå‚阅 å­˜å‚¨æ¨Ąæŋ 及å…ļ åĢ义", "storage_template_onboarding_description_v2": "å¯į”¨åŽīŧŒæ­¤åŠŸčƒŊå°†æ šæŽį”¨æˆˇåŽšäš‰įš„æ¨Ąæŋč‡ĒåŠ¨æ•´į†æ–‡äģļ。更多äŋĄæ¯īŧŒč¯ˇå‚阅 æ–‡æĄŖã€‚", "storage_template_path_length": "čŋ‘äŧŧčˇ¯åž„é•ŋåēĻ限åˆļīŧš{length, number}/{limit, number}", "storage_template_settings": "å­˜å‚¨æ¨Ąæŋ", - "storage_template_settings_description": "įŽĄį†ä¸Šäŧ čĩ„äē§æ–‡äģļ多į쓿ž„和文äģļ名", + "storage_template_settings_description": "įŽĄį†å­˜æ”žåˇ˛ä¸Šäŧ į…§į‰‡/视éĸ‘įš„æ–‡äģļ多į쓿ž„和文äģļ名", "storage_template_user_label": "{label}ä¸ēč¯Ĩį”¨æˆˇįš„å­˜å‚¨æ ‡į­ž", "system_settings": "įŗģįģŸčŽžįŊŽ", "tag_cleanup_job": "æ ‡į­žæ¸…į†", @@ -351,16 +351,16 @@ "template_settings": "通įŸĨæ¨Ąæŋ", "template_settings_description": "įŽĄį†é€šįŸĨįš„č‡ĒåŽšäš‰æ¨Ąæŋ", "theme_custom_css_settings": "č‡Ē厚䚉 CSS", - "theme_custom_css_settings_description": "CSS å…čŽ¸č‡Ē厚䚉 Immich į•ŒéĸčŽžčŽĄã€‚", + "theme_custom_css_settings_description": "äŊŋᔍCSSč‡Ē厚䚉Immichį•ŒéĸčŽžčŽĄã€‚", "theme_settings": "ä¸ģéĸ˜čŽžįŊŽ", "theme_settings_description": "č‡Ē厚䚉 Immich Web į•Œéĸ", "thumbnail_generation_job": "į”ŸæˆįŧŠį•Ĩ回", - "thumbnail_generation_job_description": "ä¸ē每ä¸Ēčĩ„äē§į”Ÿæˆä¸åŒå°ēå¯¸įš„įŧŠį•Ĩ回īŧŒåšļä¸ē每ä¸Ēäēēį‰Šį”ŸæˆįŧŠį•Ĩ回", + "thumbnail_generation_job_description": "ä¸ē每ä¸Ēᅧቇ/视éĸ‘į”Ÿæˆä¸åŒå°ēå¯¸įš„įŧŠį•Ĩ回īŧŒåšļä¸ē每ä¸Ēäēēį‰Šį”ŸæˆįŧŠį•Ĩ回", "transcoding_acceleration_api": "įĄŦäģļ加速 API", "transcoding_acceleration_api_description": "ᔍäēŽä¸ŽčŽžå¤‡äē¤äē’äģĨ加速čŊŦį įš„ API。č¯Ĩ莞įŊŽé‡‡į”¨â€œå°ŊåŠ›č€Œä¸ēâ€į­–į•Ĩīŧšč‹ĨįĄŦäģļåŠ é€Ÿå¤ąč´ĨīŧŒįŗģįģŸå°†č‡Ē动回退到čŊ¯äģļčŊŦį ã€‚VP9 įŧ–į įš„æ”¯æŒæƒ…å†ĩå–å†ŗäēŽæ‚¨įš„įĄŦäģļ配įŊŽã€‚", - "transcoding_acceleration_nvenc": "NVENCīŧˆéœ€čρ NVIDIA æ˜žåĄīŧ‰", - "transcoding_acceleration_qsv": "Quick Syncīŧˆéœ€čρ Intel 7äģŖåŠäģĨä¸Šįš„ CPUīŧ‰", - "transcoding_acceleration_rkmpp": "RKMPPīŧˆäģ…适ᔍäēŽ Rockchip SOCsīŧ‰", + "transcoding_acceleration_nvenc": "NVENCīŧˆéœ€čρNVIDIAæ˜žåĄīŧ‰", + "transcoding_acceleration_qsv": "Quick Syncīŧˆéœ€čρIntel 7äģŖåŠäģĨä¸Šįš„CPUīŧ‰", + "transcoding_acceleration_rkmpp": "RKMPPīŧˆäģ…适ᔍäēŽRockchip SOCsīŧ‰", "transcoding_acceleration_vaapi": "视éĸ‘加速 API", "transcoding_accepted_audio_codecs": "æ”¯æŒįš„éŸŗéĸ‘įŧ–᠁æ ŧåŧ", "transcoding_accepted_audio_codecs_description": "选拊无需čŊŦį įš„éŸŗéĸ‘įŧ–᠁æ ŧåŧã€‚äģ…åœ¨į‰šåŽšįš„čŊŦ᠁᭖į•Ĩä¸‹į”Ÿæ•ˆã€‚", @@ -370,11 +370,11 @@ "transcoding_accepted_video_codecs_description": "选拊无需čŊŦį įš„č§†éĸ‘įŧ–᠁æ ŧåŧã€‚äģ…åœ¨į‰šåŽšįš„čŊŦ᠁᭖į•Ĩä¸‹į”Ÿæ•ˆã€‚", "transcoding_advanced_options_description": "å¤§å¤šæ•°į”¨æˆˇä¸éœ€čĻæ›´æ”šįš„é€‰éĄš", "transcoding_audio_codec": "韺éĸ‘įŧ–᠁æ ŧåŧ", - "transcoding_audio_codec_description": "Opus æ˜¯éŸŗč´¨æœ€éĢ˜įš„é€‰éĄšīŧŒäŊ†åœ¨č€æ—§čŽžå¤‡æˆ–čŊ¯äģļä¸Šįš„å…ŧåŽšæ€§čžƒåˇŽã€‚", + "transcoding_audio_codec_description": "Opusæ˜¯éŸŗč´¨æœ€éĢ˜įš„é€‰éĄšīŧŒäŊ†åœ¨č€æ—§čŽžå¤‡æˆ–čŊ¯äģļä¸Šįš„å…ŧåŽšæ€§čžƒåˇŽã€‚", "transcoding_bitrate_description": "视éĸ‘į įŽ‡é̘äēŽæœ€å¤§é™åˆļīŧŒæˆ–æ ŧåŧä¸åœ¨æŽĨå—åˆ—čĄ¨ä¸­", "transcoding_codecs_learn_more": "č‹Ĩčρäē†č§Ŗæ­¤å¤„äŊŋį”¨įš„æœ¯č¯­č¯Ļ情īŧŒč¯ˇæŸĨ阅 FFmpeg æ–‡æĄŖä¸­įš„ H.264 įŧ–į ã€HEVC įŧ–᠁ 和 VP9 įŧ–į ã€‚", "transcoding_constant_quality_mode": "æ’åŽšč´¨é‡æ¨Ąåŧ", - "transcoding_constant_quality_mode_description": "ICQ 比 CQP 效果更åĨŊīŧŒäŊ†éƒ¨åˆ†įĄŦäģļåŠ é€ŸčŽžå¤‡ä¸æ”¯æŒæ­¤æ¨Ąåŧã€‚吝ᔍč¯Ĩé€‰éĄšåŽīŧŒåœ¨åŸēäēŽč´¨é‡įš„įŧ–᠁䏭将äŧ˜å…ˆäŊŋį”¨æŒ‡åŽšįš„æ¨Ąåŧã€‚į”ąäēŽ NVENCīŧˆNVIDIA æ˜žåĄįŧ–᠁噍īŧ‰ä¸æ”¯æŒ ICQīŧŒå› æ­¤č¯Ĩ莞įŊŽå¯šå…ļ无效。", + "transcoding_constant_quality_mode_description": "ICQ比CQP效果更åĨŊīŧŒäŊ†éƒ¨åˆ†įĄŦäģļåŠ é€ŸčŽžå¤‡ä¸æ”¯æŒæ­¤æ¨Ąåŧã€‚吝ᔍč¯Ĩé€‰éĄšåŽīŧŒåœ¨åŸēäēŽč´¨é‡įš„įŧ–᠁䏭将äŧ˜å…ˆäŊŋį”¨æŒ‡åŽšįš„æ¨Ąåŧã€‚į”ąäēŽNVENCīŧˆNVIDIAæ˜žåĄįŧ–᠁噍īŧ‰ä¸æ”¯æŒICQīŧŒå› æ­¤č¯Ĩ莞įŊŽå¯šå…ļ无效。", "transcoding_constant_rate_factor": "æ’åŽšį įŽ‡įŗģ数īŧˆ-crfīŧ‰", "transcoding_constant_rate_factor_description": "视éĸ‘č´¨é‡į­‰įē§ã€‚典型å€ŧä¸ēīŧšH.264 äŊŋᔍ 23īŧŒHEVC äŊŋᔍ 28īŧŒVP9 äŊŋᔍ 31īŧŒAV1 äŊŋᔍ 35。数å€ŧčļŠäŊŽč´¨é‡čļŠåĨŊīŧŒäŊ†į”Ÿæˆįš„æ–‡äģļ也čļŠå¤§ã€‚", "transcoding_disabled_description": "不čŊŦ᠁äģģäŊ•视éĸ‘īŧŒå¯čƒŊäŧšå¯ŧč‡´éƒ¨åˆ†åŽĸæˆˇįĢ¯æ— æŗ•æ’­æ”ž", @@ -394,7 +394,7 @@ "transcoding_policy": "čŊŦ᠁᭖į•Ĩ", "transcoding_policy_description": "莞įŊŽč§†éĸ‘čŊŦ᠁æ—ļæœē", "transcoding_preferred_hardware_device": "éϖ选įĄŦäģļčŽžå¤‡", - "transcoding_preferred_hardware_device_description": "äģ…适ᔍäēŽ VAAPI 和 QSVã€‚čŽžįŊŽį”¨äēŽįĄŦäģļčŊŦį įš„ DRI čŽžå¤‡čŠ‚į‚šã€‚", + "transcoding_preferred_hardware_device_description": "äģ…适ᔍäēŽVAAPI和QSVã€‚čŽžįŊŽį”¨äēŽįĄŦäģļčŊŦį įš„DRIčŽžå¤‡čŠ‚į‚šã€‚", "transcoding_preset_preset": "éĸ„莞īŧˆ-presetīŧ‰", "transcoding_preset_preset_description": "压įŧŠé€ŸåēĻ。éĸ„čŽžé€ŸåēĻč…ĸīŧŒį”Ÿæˆįš„æ–‡äģļčļŠå°īŧ›åœ¨čŽžåŽšį‰šåŽšį įŽ‡æ—ļīŧŒčŋ˜čƒŊ提升į”ģč´¨ã€‚VP9 įŧ–᠁噍äŧšåŋŊį•Ĩīŧˆä¸æ”¯æŒīŧ‰é̘äēŽâ€œfaster”速åēĻįš„é€‰éĄšã€‚", "transcoding_reference_frames": "å‚č€ƒå¸§", @@ -405,7 +405,7 @@ "transcoding_target_resolution": "į›Žæ ‡åˆ†čž¨įŽ‡", "transcoding_target_resolution_description": "更éĢ˜įš„åˆ†čž¨įŽ‡č™Ŋį„ļčƒŊäŋį•™æ›´å¤šį”ģéĸįģ†čŠ‚īŧŒäŊ†äŧšåģļé•ŋįŧ–᠁æ—ļ间、åĸžå¤§æ–‡äģļäŊ“᧝īŧŒåšļ可čƒŊå¯ŧ致åē”į”¨å“åē”变æ…ĸ。", "transcoding_temporal_aq": "æ—ļ间域č‡Ē适åē”量化", - "transcoding_temporal_aq_description": "äģ…适ᔍäēŽ NVENC。æ—ļ间域č‡Ē适åē”量化可提升é̘įģ†čŠ‚ã€äŊŽčŋåЍåœēæ™¯įš„į”ģč´¨ã€‚å¯čƒŊä¸Žčžƒæ—§įš„čŽžå¤‡ä¸å…ŧ厚。", + "transcoding_temporal_aq_description": "äģ…适ᔍäēŽNVENC。æ—ļ间域č‡Ē适åē”量化可提升é̘įģ†čŠ‚ã€äŊŽčŋåЍåœēæ™¯įš„į”ģč´¨ã€‚å¯čƒŊä¸Žčžƒæ—§įš„čŽžå¤‡ä¸å…ŧ厚。", "transcoding_threads": "įēŋį¨‹æ•°", "transcoding_threads_description": "数å€ŧčļŠé̘īŧŒįŧ–į é€ŸåēĻčļŠåŋĢīŧŒäŊ†åœ¨čŋčĄŒæ—ļäŧšå‡å°‘æœåŠĄå™¨å¤„į†å…ļäģ–äģģåŠĄįš„äŊ™é‡ã€‚č¯Ĩ数å€ŧ不åē”čļ…čŋ‡ CPU æ ¸åŋƒæ•°ã€‚莞ä¸ē 0 可最大化čĩ„æēåˆŠį”¨įŽ‡ã€‚", "transcoding_tone_mapping": "č‰˛č°ƒæ˜ å°„", @@ -415,7 +415,7 @@ "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.265īŧ‰æ€§čƒŊä¸Žäš‹į›¸äŧŧīŧŒäŊ†åœ¨įŊ‘éĄĩįĢ¯įš„å…ŧåŽšæ€§čžƒåˇŽã€‚H.264 å…ŧ厚性极åšŋ且čŊŦį é€ŸåēĻåŋĢīŧŒäŊ†į”Ÿæˆįš„æ–‡äģļäŊ“᧝čĻå¤§åž—å¤šã€‚AV1 æ˜¯æ•ˆįŽ‡æœ€éĢ˜įš„įŧ–᠁æ ŧåŧīŧŒäŊ†åœ¨æ—§čŽžå¤‡ä¸Šįŧē䚏支持。", + "transcoding_video_codec_description": "VP9įŧ–į æ•ˆįŽ‡é̘īŧŒä¸”在įŊ‘éĄĩį̝å…ŧ厚性åĨŊīŧŒäŊ†čŊŦ᠁耗æ—ļ螃é•ŋ。HEVCīŧˆH.265īŧ‰æ€§čƒŊä¸Žäš‹į›¸äŧŧīŧŒäŊ†åœ¨įŊ‘éĄĩįĢ¯įš„å…ŧåŽšæ€§čžƒåˇŽã€‚H.264å…ŧ厚性极åšŋ且čŊŦį é€ŸåēĻåŋĢīŧŒäŊ†į”Ÿæˆįš„æ–‡äģļäŊ“᧝čĻå¤§åž—å¤šã€‚AV1æ˜¯æ•ˆįŽ‡æœ€éĢ˜įš„įŧ–᠁æ ŧåŧīŧŒäŊ†åœ¨æ—§čŽžå¤‡ä¸Šįŧē䚏支持。", "trash_enabled_description": "å¯į”¨å›žæ”ļįĢ™åŠŸčƒŊ", "trash_number_of_days": "äŋį•™å¤Šæ•°", "trash_number_of_days_description": "文äģļ在回æ”ļį̙䏭äŋį•™å¤šå°‘夊后čĸĢæ°¸äš…删除", @@ -425,11 +425,11 @@ "unlink_all_oauth_accounts_description": "在čŋį§ģåˆ°æ–°æœåŠĄå•†äš‹å‰īŧŒč¯ˇčŽ°åž—č§Ŗé™¤æ‰€æœ‰ OAuth č´Ļæˆˇįš„å…ŗč”ã€‚", "unlink_all_oauth_accounts_prompt": "æ‚¨įĄŽåŽščĻč§Ŗé™¤æ‰€æœ‰ OAuth č´Ļæˆˇįš„å…ŗč”å—īŧŸæ­¤æ“äŊœå°†é‡įŊŽæ¯ä¸Ēį”¨æˆˇįš„čēĢäģŊčŽ¤č¯ IDīŧŒä¸”æ— æŗ•æ’¤é”€ã€‚", "user_cleanup_job": "į”¨æˆˇæ¸…į†", - "user_delete_delay": "{user}įš„č´ĻæˆˇåŠčĩ„äē§å°†åœ¨{delay, plural, one {#夊} other {#夊}}后čĸĢ厉排永䚅删除。", + "user_delete_delay": "{user}įš„č´ĻæˆˇåŠčĩ„äē§å°†åœ¨{delay, plural, one {#夊} other {#夊}}后čĸĢæ°¸äš…删除。", "user_delete_delay_settings": "åģᅵŸåˆ é™¤", - "user_delete_delay_settings_description": "į§ģ除后多少夊īŧŒæ°¸äš…åˆ é™¤į”¨æˆˇįš„č´ĻæˆˇåŠčĩ„äē§ã€‚į”¨æˆˇåˆ é™¤äģģåŠĄå°†åœ¨åˆå¤œčŋčĄŒīŧŒäģĨæŖ€æŸĨ是åĻæœ‰åž…åˆ é™¤įš„į”¨æˆˇã€‚æ­¤čŽžįŊŽįš„æ›´æ”šå°†åœ¨ä¸‹æŦĄäģģåŠĄæ‰§čĄŒæ—ļį”Ÿæ•ˆã€‚", - "user_delete_immediately": "{user}įš„č´ĻæˆˇåŠčĩ„äē§å°†čĸĢįĢ‹åŗåŽ‰æŽ’æ°¸äš…åˆ é™¤ã€‚", - "user_delete_immediately_checkbox": "å°†į”¨æˆˇåŠå…ļčĩ„äē§åŠ å…ĨįĢ‹åŗåˆ é™¤é˜Ÿåˆ—", + "user_delete_delay_settings_description": "į§ģé™¤åŽæ°¸äš…åˆ é™¤į”¨æˆˇįš„č´ĻæˆˇåŠæ–‡äģļįš„å¤Šæ•°ã€‚į”¨æˆˇåˆ é™¤äģģåŠĄå°†åœ¨æˇąå¤œæŖ€æŸĨåž…åˆ é™¤įš„į”¨æˆˇã€‚æ­¤čŽžįŊŽįš„æ›´æ”šå°†åœ¨ä¸‹æŦĄäģģåŠĄæ‰§čĄŒæ—ļį”Ÿæ•ˆã€‚", + "user_delete_immediately": "{user}įš„č´ĻæˆˇåŠčĩ„äē§å°†čĸĢįĢ‹åŗæ°¸äš…åˆ é™¤ã€‚", + "user_delete_immediately_checkbox": "å°†į”¨æˆˇåŠå…ļ文äģļ加å…ĨįĢ‹åŗåˆ é™¤é˜Ÿåˆ—", "user_details": "į”¨æˆˇč¯Ļ情", "user_management": "į”¨æˆˇįŽĄį†", "user_password_has_been_reset": "į”¨æˆˇįš„å¯†į åˇ˛é‡įŊŽīŧš", @@ -441,7 +441,7 @@ "user_successfully_removed": "į”¨æˆˇ {email} åˇ˛æˆåŠŸåˆ é™¤ã€‚", "users_page_description": "įŽĄį†į”¨æˆˇéĄĩéĸ", "version_check_enabled_description": "æŖ€æŸĨčŊ¯äģļæ–°į‰ˆæœŦ", - "version_check_implications": "į‰ˆæœŦæŖ€æŸĨ功čƒŊ䞝čĩ–äēŽä¸Ž github.com įš„åŽšæœŸé€šäŋĄ", + "version_check_implications": "į‰ˆæœŦæŖ€æŸĨ功čƒŊ䞝čĩ–äēŽä¸Ž {server} įš„åŽšæœŸé€šäŋĄ", "version_check_settings": "æ–°į‰ˆæœŦæŖ€æŸĨ", "version_check_settings_description": "吝ᔍ/įĻį”¨æ–°į‰ˆæœŦ通įŸĨ", "video_conversion_job": "čŊŦ᠁视éĸ‘", @@ -454,10 +454,10 @@ "advanced_settings_clear_image_cache": "清įŠē回像įŧ“å­˜", "advanced_settings_clear_image_cache_error": "æ— æŗ•æ¸…įŠē回像įŧ“å­˜", "advanced_settings_clear_image_cache_success": "æˆåŠŸæ¸…į† {size}", - "advanced_settings_enable_alternate_media_filter_subtitle": "äŊŋį”¨æ­¤é€‰éĄšå¯æ šæŽå…ļäģ–æĄäģļį­›é€‰åŒæ­ĨæœŸé—´įš„åĒ’äŊ“。äģ…在åē”į”¨æ— æŗ•æŖ€æĩ‹åˆ°æ‰€æœ‰į›¸å†Œæ—ļå°č¯•æ­¤é€‰éĄšã€‚", + "advanced_settings_enable_alternate_media_filter_subtitle": "äŊŋį”¨æ­¤é€‰éĄšå¯æ šæŽæ›ŋäģŖæ ‡å‡†åœ¨åŒæ­Ĩ期间čŋ‡æģ¤åĒ’äŊ“。äģ…åŊ“åē”į”¨æ— æŗ•æŖ€æĩ‹æ‰€æœ‰į›¸å†Œæ—ļīŧŒæ‰å°č¯•äŊŋį”¨æ­¤é€‰éĄšã€‚", "advanced_settings_enable_alternate_media_filter_title": "[厞éĒŒæ€§] äŊŋį”¨å¤‡į”¨čŽžå¤‡į›¸å†Œį­›é€‰æ–šåŧ", "advanced_settings_log_level_title": "æ—Ĩåŋ—į­‰įē§: {level}", - "advanced_settings_prefer_remote_subtitle": "éƒ¨åˆ†čŽžå¤‡č¯ģ取æœŦ地čĩ„æēįŧŠį•Ĩå›žįš„é€ŸåēĻæžæ…ĸ。åŧ€å¯æ­¤čŽžįŊŽå¯æ”šä¸ē加čŊŊčŋœį¨‹å›žį‰‡ã€‚", + "advanced_settings_prefer_remote_subtitle": "éƒ¨åˆ†čŽžå¤‡č¯ģ取æœŦ地文äģļįŧŠį•Ĩå›žįš„é€ŸåēĻæžæ…ĸ。åŧ€å¯æ­¤čŽžįŊŽå¯æ”šä¸ē加čŊŊčŋœį¨‹å›žį‰‡ã€‚", "advanced_settings_prefer_remote_title": "äŧ˜å…ˆäŊŋᔍčŋœį¨‹å›žį‰‡", "advanced_settings_proxy_headers_subtitle": "厚䚉 Immich 每æŦĄįŊ‘įģœč¯ˇæą‚åē”附å¸Ļįš„äģŖį†å¤´äŋĄæ¯", "advanced_settings_proxy_headers_title": "č‡Ē厚䚉äģŖį†å¤´äŋĄæ¯ [厞éĒŒæ€§]", @@ -474,8 +474,8 @@ "age_year_months": "1垁{months, plural, one {#ä¸Ē月} other {#ä¸Ē月}}", "age_years": "{years, plural, other {#垁}}", "album": "į›¸å†Œ", - "album_added": "į›¸å†Œæˇģ加成功", - "album_added_notification_setting_description": "åŊ“您čĸĢæˇģåŠ åˆ°å…ąäēĢį›¸å†Œæ—ļīŧŒæŽĨæ”ļé‚ŽįŽąé€šįŸĨ", + "album_added": "į›¸å†Œåˇ˛æˇģ加", + "album_added_notification_setting_description": "åŊ“您čĸĢæˇģåŠ åˆ°å…ąäēĢį›¸å†Œæ—ļīŧŒé€ščŋ‡é‚Žäģļ通įŸĨ", "album_cover_updated": "封éĸåˇ˛æ›´æ–°", "album_delete_confirmation": "įĄŽåŽščĻåˆ é™¤į›¸å†Œ “{album}” 吗īŧŸ", "album_delete_confirmation_description": "åĻ‚æžœæ­¤į›¸å†Œåˇ˛čĸĢå…ąäēĢīŧŒå…ļäģ–į”¨æˆˇäšŸå°†æ— æŗ•å†čŽŋ闎厃。", @@ -491,10 +491,10 @@ "album_remove_user_confirmation": "įĄŽåŽščρį§ģ除 “{user}” 吗īŧŸ", "album_search_not_found": "æœĒ扞到与搜į´ĸæĄäģļåŒšé…įš„į›¸å†Œ", "album_selected": "į›¸å†Œåˇ˛é€‰ä¸­", - "album_share_no_users": "įœ‹čĩˇæĨæ‚¨åˇ˛å°†æ­¤į›¸å†Œå…ąäēĢį왿‰€æœ‰į”¨æˆˇīŧŒæˆ–č€…æ‚¨æ˛Ąæœ‰å¯å…ąäēĢįš„į”¨æˆˇã€‚", + "album_share_no_users": "æ‚¨åˇ˛å°†æ­¤į›¸å†Œå…ąäēĢį왿‰€æœ‰į”¨æˆˇīŧŒæˆ–æ˛Ąæœ‰å¯å…ąäēĢįš„į”¨æˆˇã€‚", "album_summary": "į›¸å†ŒæĻ‚č§ˆ", "album_updated": "į›¸å†Œåˇ˛æ›´æ–°", - "album_updated_setting_description": "åŊ“å…ąäēĢį›¸å†Œæœ‰æ–°å†…åŽšæ—ļīŧŒæŽĨæ”ļ邮äģļ通įŸĨ", + "album_updated_setting_description": "åŊ“å…ąäēĢį›¸å†Œæœ‰æ–°å†…åŽšæ—ļīŧŒé€ščŋ‡é‚Žäģļ通įŸĨ", "album_upload_assets": "äģŽæ‚¨įš„į”ĩ脑上äŧ æ–‡äģļåšļæˇģåŠ åˆ°į›¸å†Œ", "album_user_left": "厞退å‡ē “{album}”", "album_user_removed": "厞į§ģ除 “{user}”", @@ -508,12 +508,12 @@ "album_viewer_page_share_add_users": "邀蝎äģ–äēē", "album_with_link_access": "å…čŽ¸äģģäŊ•æ‹Ĩ有č¯Ĩ链æŽĨįš„äē翟Ĩįœ‹æ­¤į›¸å†Œä¸­įš„į…§į‰‡å’Œäēēį‰Šã€‚", "albums": "į›¸å†Œ", - "albums_count": "{count, plural, one {{count, number} ä¸Ēį›¸å†Œ} other {{count, number} ä¸Ēį›¸å†Œ}}", + "albums_count": "{count, plural, one {{count, number}ä¸Ēį›¸å†Œ} other {{count, number}ä¸Ēį›¸å†Œ}}", "albums_default_sort_order": "éģ˜čŽ¤į›¸å†ŒæŽ’åēæ–šåŧ", - "albums_default_sort_order_description": "创åģēæ–°į›¸å†Œæ—ļīŧŒåŊąåƒįš„初始排åēæ–šåŧã€‚", + "albums_default_sort_order_description": "创åģēæ–°į›¸å†Œæ—ļīŧŒčĩ„æēįš„初始排åēæ–šåŧã€‚", "albums_feature_description": "可与å…ļäģ–į”¨æˆˇå…ąäēĢįš„į…§į‰‡/内厚合集。", "albums_on_device_count": "čŽžå¤‡ä¸Šįš„į›¸å†Œīŧˆ{count} ä¸Ēīŧ‰", - "albums_selected": "{count, plural, one {# ä¸Ēį›¸å†Œåˇ˛é€‰æ‹Š} other {# ä¸Ēį›¸å†Œåˇ˛é€‰æ‹Š}}", + "albums_selected": "{count, plural, one {厞选䏭#ä¸Ēį›¸å†Œ} other {厞选䏭#ä¸Ēį›¸å†Œ}}", "all": "全部", "all_albums": "æ‰€æœ‰į›¸å†Œ", "all_people": "全部äēēį‰Š", @@ -529,10 +529,10 @@ "always_keep_photos_hint": "åŧ€å¯â€œé‡Šæ”žįŠē间”后īŧŒäģäŧšäŋį•™æ‰€æœ‰į…§į‰‡åœ¨æœŦčŽžå¤‡ä¸Šã€‚", "always_keep_videos_hint": "åŧ€å¯â€œé‡Šæ”žįŠē间”后īŧŒäģäŧšäŋį•™æ‰€æœ‰č§†éĸ‘在æœŦčŽžå¤‡ä¸Šã€‚", "anti_clockwise": "逆æ—ļ针", - "api_key": "API 密é’Ĩ", + "api_key": "API密é’Ĩ", "api_key_description": "č¯Ĩåē”ᔍ坆é’ĨåĒäŧšæ˜žį¤ē一æŦĄã€‚č¯ˇįĄŽäŋåœ¨å…ŗé—­įĒ—åŖå‰å¤åˆļ下æĨ。", - "api_key_empty": "API 密é’Ĩåį§°ä¸å¯ä¸ēįŠē", - "api_keys": "API 密é’Ĩ", + "api_key_empty": "API密é’Ĩåį§°ä¸å¯ä¸ēįŠē", + "api_keys": "API密é’Ĩ", "app_architecture_variant": "变äŊ“īŧˆæžļ构īŧ‰", "app_bar_signout_dialog_content": "æ‚¨įĄŽåŽščρ退å‡ē吗īŧŸ", "app_bar_signout_dialog_ok": "是", @@ -551,64 +551,64 @@ "archive_size": "åŊ’æĄŖå¤§å°", "archive_size_description": "配įŊŽä¸‹čŊŊįš„åŊ’æĄŖå¤§å°īŧˆGiBīŧ‰", "archived": "厞åŊ’æĄŖ", - "archived_count": "{count, plural, other {厞åŊ’æĄŖ # 饚}}", + "archived_count": "{count, plural, other {厞åŊ’æĄŖ#饚}}", "are_these_the_same_person": "čŋ™æ˜¯åŒä¸€ä¸Ēäēē吗īŧŸ", "are_you_sure_to_do_this": "įĄŽåŽščĻæ‰§čĄŒæ­¤æ“äŊœīŧŸ", "array_field_not_fully_supported": "数įģ„å­—æŽĩ需čĻæ‰‹åŠ¨čŋ›čĄŒ JSON įŧ–čž‘", - "asset_action_delete_err_read_only": "æ— æŗ•åˆ é™¤åĒč¯ģčĩ„æēīŧŒåˇ˛čˇŗčŋ‡", - "asset_action_share_err_offline": "æ— æŗ•čŽˇå–įĻģįēŋčĩ„æēīŧŒåˇ˛čˇŗčŋ‡", + "asset_action_delete_err_read_only": "æ— æŗ•åˆ é™¤åĒč¯ģéĄšį›ŽīŧŒåˇ˛čˇŗčŋ‡", + "asset_action_share_err_offline": "æ— æŗ•čŽˇå–įĻģįēŋéĄšį›ŽīŧŒåˇ˛čˇŗčŋ‡", "asset_added_to_album": "厞æˇģåŠ č‡ŗį›¸å†Œ", "asset_adding_to_album": "æ­Ŗåœ¨æˇģåŠ č‡ŗį›¸å†Œâ€Ļ", - "asset_created": "čĩ„æēåˇ˛åˆ›åģē", - "asset_description_updated": "čĩ„æēæčŋ°åˇ˛æ›´æ–°", - "asset_filename_is_offline": "čĩ„æēâ€œ{filename}â€åˇ˛įĻģįēŋ", - "asset_has_unassigned_faces": "čĩ„æēåŒ…åĢæœĒåˆ†é…įš„äēē脸", + "asset_created": "éĄšį›Žåˇ˛åˆ›åģē", + "asset_description_updated": "éĄšį›Žæčŋ°åˇ˛æ›´æ–°", + "asset_filename_is_offline": "éĄšį›Ž{filename}厞įĻģįēŋ", + "asset_has_unassigned_faces": "éĄšį›ŽåŒ…åĢæœĒåˆ†é…įš„äēē脸", "asset_hashing": "æ­Ŗåœ¨čŽĄįŽ—å“ˆå¸Œå€ŧâ€Ļ", "asset_list_group_by_sub_title": "分įģ„䞝捎", "asset_list_layout_settings_dynamic_layout_title": "åŠ¨æ€å¸ƒåą€", "asset_list_layout_settings_group_automatically": "č‡Ē动", - "asset_list_layout_settings_group_by": "čĩ„æēåˆ†įģ„䞝捎", + "asset_list_layout_settings_group_by": "ᅧቇ/视éĸ‘分įģ„䞝捎", "asset_list_layout_settings_group_by_month_day": "月äģŊ + æ—Ĩ期", "asset_list_layout_sub_title": "å¸ƒåą€", "asset_list_settings_subtitle": "ᅧቇįŊ‘æ ŧå¸ƒåą€čŽžįŊŽ", "asset_list_settings_title": "ᅧቇįŊ‘æ ŧ", - "asset_not_found_on_device_android": "čŽžå¤‡ä¸ŠæœĒ扞到č¯Ĩčĩ„æē", - "asset_not_found_on_device_ios": "čŽžå¤‡ä¸ŠæœĒ扞到č¯Ĩčĩ„æēã€‚åĻ‚æžœæ‚¨äŊŋᔍäē† iCloudīŧŒå¯čƒŊæ˜¯į”ąäēŽ iCloud 中存储äē†é”™č¯¯įš„æ–‡äģļå¯ŧ致čĩ„æēæ— æŗ•čŽŋ问", - "asset_not_found_on_icloud": "iCloud 中æœĒ扞到č¯Ĩčĩ„æēã€‚可čƒŊæ˜¯į”ąäēŽ iCloud 中存储äē†é”™č¯¯įš„æ–‡äģļå¯ŧ致čĩ„æēæ— æŗ•čŽŋ问", - "asset_offline": "čĩ„æēįĻģįēŋ", - "asset_offline_description": "įŖį›˜ä¸ŠæœĒ扞到此外部čĩ„æēã€‚蝎联įŗģæ‚¨įš„ Immich įŽĄį†å‘˜å¯ģæą‚å¸ŽåŠŠã€‚", - "asset_restored_successfully": "čĩ„æēæĸ复成功", + "asset_not_found_on_device_android": "čŽžå¤‡ä¸ŠæœĒ扞到č¯Ĩᅧቇ/视éĸ‘", + "asset_not_found_on_device_ios": "čŽžå¤‡ä¸ŠæœĒ扞到č¯Ĩᅧቇ/视éĸ‘。åĻ‚æžœæ‚¨äŊŋᔍäē† iCloudīŧŒå¯čƒŊæ˜¯į”ąäēŽ iCloud 中存储äē†é”™č¯¯įš„æ–‡äģļå¯ŧ致čĩ„æēæ— æŗ•čŽŋ问", + "asset_not_found_on_icloud": "iCloud中æœĒ扞到č¯Ĩᅧቇ/视éĸ‘。可čƒŊæ˜¯į”ąäēŽiCloud中存储äē†é”™č¯¯įš„æ–‡äģļå¯ŧ致čĩ„æēæ— æŗ•čŽŋ问", + "asset_offline": "éĄšį›ŽįĻģįēŋ", + "asset_offline_description": "įŖį›˜ä¸ŠæœĒ扞到此外部文äģļã€‚č¯ˇč”įŗģæ‚¨įš„ Immich įŽĄį†å‘˜å¯ģæą‚å¸ŽåŠŠã€‚", + "asset_restored_successfully": "文äģ￁ĸ复成功", "asset_skipped": "厞莺čŋ‡", "asset_skipped_in_trash": "在回æ”ļį̙䏭", - "asset_trashed": "čĩ„æēåˇ˛į§ģč‡ŗå›žæ”ļįĢ™", - "asset_troubleshoot": "čĩ„æēč¯Šæ–­", + "asset_trashed": "文äģļ厞į§ģč‡ŗå›žæ”ļįĢ™", + "asset_troubleshoot": "文äģļč¯Šæ–­", "asset_uploaded": "厞䏊äŧ ", "asset_uploading": "上äŧ ä¸­â€Ļ", "asset_viewer_settings_subtitle": "įŽĄį†į”ģå슿ŸĨįœ‹å™¨čŽžįŊŽ", - "asset_viewer_settings_title": "čĩ„æēæŸĨįœ‹å™¨", + "asset_viewer_settings_title": "文äģ￟Ĩįœ‹å™¨", "assets": "čĩ„æē", - "assets_added_count": "厞æˇģ加{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}", + "assets_added_count": "厞æˇģ加{count, plural, one {#ä¸Ē文äģļ} other {#ä¸Ē文äģļ}}", "assets_added_to_album_count": "åˇ˛å‘į›¸å†Œæˇģ加{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}", - "assets_added_to_albums_count": "厞向 {albumTotal, plural, one {# ä¸Ēį›¸å†Œ} other {# ä¸Ēį›¸å†Œ}}æˇģ加 {assetTotal, plural, one {# ä¸Ēčĩ„æē} other {# ä¸Ēčĩ„æē}}", + "assets_added_to_albums_count": "厞向 {albumTotal, plural, one {# ä¸Ēį›¸å†Œ} other {# ä¸Ēį›¸å†Œ}}æˇģ加 {assetTotal, plural, one {# ä¸Ēčĩ„æē} other {# ä¸ĒåŒģé™ĸ}}", "assets_cannot_be_added_to_album_count": "æ— æŗ•å‘į›¸å†Œæˇģ加{count, plural, one {ä¸Ēčĩ„æē} other {ä¸Ēčĩ„æē}}", "assets_cannot_be_added_to_albums": "æ— æŗ•å‘äģģäŊ•一ä¸Ēį›¸å†Œæˇģ加 {count, plural, one {ä¸Ēčĩ„æē} other {ä¸Ēčĩ„æē}}", - "assets_count": "{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}", - "assets_deleted_permanently": "åˇ˛æ°¸äš…åˆ é™¤ {count} ä¸Ēčĩ„æē", - "assets_deleted_permanently_from_server": "åˇ˛æ°¸äš…į§ģ除 {count} ä¸Ēčĩ„äē§", - "assets_downloaded_failed": "{count, plural, one {厞䏋čŊŊ#ä¸Ē文äģļ - {error} ä¸Ē文äģļ下čŊŊå¤ąč´Ĩ} other {厞䏋čŊŊ#ä¸Ē文äģļ - {error} ä¸Ē文äģļ下čŊŊå¤ąč´Ĩ}}", - "assets_downloaded_successfully": "{count, plural, one {åˇ˛æˆåŠŸä¸‹čŊŊ # ä¸Ē文äģļ} other {åˇ˛æˆåŠŸä¸‹čŊŊ # ä¸Ē文äģļ}}", - "assets_moved_to_trash_count": "厞将{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}į§ģ动到回æ”ļįĢ™", - "assets_permanently_deleted_count": "åˇ˛æ°¸äš…åˆ é™¤{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}", - "assets_removed_count": "厞į§ģ除{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}", - "assets_removed_permanently_from_device": "厞äģŽæ‚¨įš„čŽžå¤‡ä¸­æ°¸äš…åˆ é™¤ {count} ä¸Ēčĩ„æē", - "assets_restore_confirmation": "æ‚¨įĄŽåŽščρæĸ复回æ”ļįĢ™ä¸­įš„æ‰€æœ‰čĩ„æēå—īŧŸæ­¤æ“äŊœæ— æŗ•撤销īŧč¯ˇæŗ¨æ„īŧŒäģģäŊ•įĻģįēŋčĩ„æēæ— æŗ•通čŋ‡æ­¤æ–šåŧæĸ复。", - "assets_restored_count": "厞æĸ复{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}", - "assets_restored_successfully": "åˇ˛æˆåŠŸæĸ复{count}ä¸Ēčĩ„æē", - "assets_trashed": "{count} ä¸Ēčĩ„æēį§ģč‡ŗå›žæ”ļįĢ™", - "assets_trashed_count": "厞将{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}į§ģč‡ŗå›žæ”ļįĢ™", - "assets_trashed_from_server": "Immich æœåŠĄå™¨ä¸Šåˇ˛į§ģ除 {count} ä¸Ēčĩ„æē", - "assets_were_part_of_album_count": "{count, plural, one {ä¸Ēčĩ„æē} other {ä¸Ēčĩ„æē}}厞圍č¯Ĩį›¸å†Œä¸­", - "assets_were_part_of_albums_count": "{count, plural, one {ä¸Ēčĩ„æē} other {ä¸Ēčĩ„æē}} 厞存圍äēŽčŋ™äē›į›¸å†Œä¸­", + "assets_count": "{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}", + "assets_deleted_permanently": "åˇ˛æ°¸äš…åˆ é™¤ {count} ä¸Ē文äģļ", + "assets_deleted_permanently_from_server": "åˇ˛æ°¸äš…į§ģ除 {count} ä¸Ē文äģļ", + "assets_downloaded_failed": "{count, plural, one {厞䏋čŊŊ#ä¸Ē文äģļ - {error}ä¸Ē文äģļ下čŊŊå¤ąč´Ĩ} other {厞䏋čŊŊ#ä¸Ē文äģļ - {error}ä¸Ē文äģļ下čŊŊå¤ąč´Ĩ}}", + "assets_downloaded_successfully": "{count, plural, one {åˇ˛æˆåŠŸä¸‹čŊŊ#ä¸Ē文äģļ} other {åˇ˛æˆåŠŸä¸‹čŊŊ#ä¸Ē文äģļ}}", + "assets_moved_to_trash_count": "厞将{count, plural, one {#ä¸Ē文äģļ} other {#ä¸Ē文äģļ}}į§ģ动到回æ”ļįĢ™", + "assets_permanently_deleted_count": "åˇ˛æ°¸äš…åˆ é™¤{count, plural, one {#ä¸Ē文äģļ} other {#ä¸Ē文äģļ}}", + "assets_removed_count": "厞į§ģ除{count, plural, one {#ä¸Ē文äģļ} other {#ä¸Ē文äģļ}}", + "assets_removed_permanently_from_device": "厞äģŽæ‚¨įš„čŽžå¤‡ä¸­æ°¸äš…åˆ é™¤{count}ä¸Ē文äģļ", + "assets_restore_confirmation": "æ‚¨įĄŽåŽščρæĸ复回æ”ļįĢ™ä¸­įš„æ‰€æœ‰æ–‡äģļ吗īŧŸæ­¤æ“äŊœæ— æŗ•撤销īŧč¯ˇæŗ¨æ„īŧŒįĻģįēŋ文äģļæ— æŗ•通čŋ‡æ­¤æ–šåŧæĸ复。", + "assets_restored_count": "厞æĸ复{count, plural, one {#ä¸Ē文äģļ} other {#ä¸Ē文äģļ}}", + "assets_restored_successfully": "åˇ˛æˆåŠŸæĸ复{count}ä¸Ē文äģļ", + "assets_trashed": "{count}ä¸Ē文äģļį§ģč‡ŗå›žæ”ļįĢ™", + "assets_trashed_count": "厞将{count, plural, one {#ä¸Ē文äģļ} other {#ä¸Ē文äģļ}}į§ģč‡ŗå›žæ”ļįĢ™", + "assets_trashed_from_server": "厞äģŽImmichæœåŠĄå™¨ä¸Šį§ģ除{count}ä¸Ē文äģļ", + "assets_were_part_of_album_count": "{count, plural, one {ä¸ĒåŒģé™ĸ} other {ä¸Ēčĩ„æē}}厞圍č¯Ĩį›¸å†Œä¸­", + "assets_were_part_of_albums_count": "{count, plural, one {ä¸ĒéĄšį›Ž} other {ä¸ĒéĄšį›Ž}}厞存圍äēŽčŋ™äē›į›¸į°ŋ中", "authorized_devices": "åˇ˛æŽˆæƒčŽžå¤‡", "automatic_endpoint_switching_subtitle": "åœ¨å¯į”¨æ—ļ通čŋ‡æŒ‡åŽšįš„ Wi-Fi čŋ›čĄŒæœŦ地čŋžæŽĨīŧŒå…ļäģ–äŊįŊŽåˆ™äŊŋᔍæ›ŋäģŖįŊ‘įģœčŋžæŽĨ", "automatic_endpoint_switching_title": "č‡Ē动切æĸ URL", @@ -617,31 +617,31 @@ "back_close_deselect": "čŋ”å›žã€å…ŗé—­æˆ–å–æļˆé€‰æ‹Š", "background_backup_running_error": "后台备äģŊæ­Ŗåœ¨čŋčĄŒä¸­īŧŒæ— æŗ•启动手动备äģŊ", "background_location_permission": "后台厚äŊæƒé™", - "background_location_permission_content": "ä¸ēäē†åœ¨åŽå°čŋčĄŒæ—ļåŽžįŽ°įŊ‘įģœåˆ‡æĸīŧŒImmich åŋ…éĄģ始į숿‹Ĩæœ‰į˛žįĄŽäŊįŊŽčŽŋ闎权限īŧŒäģĨäžŋåē”ᔍčƒŊ够č¯ģ取 Wi-Fi įŊ‘įģœįš„åį§°", + "background_location_permission_content": "ä¸ēäē†åœ¨åŽå°čŋčĄŒæ—ļåŽžįŽ°įŊ‘įģœåˆ‡æĸīŧŒImmichåŋ…éĄģ始į숿‹Ĩæœ‰į˛žįĄŽäŊįŊŽčŽŋ闎权限īŧŒäģĨäžŋåē”ᔍčƒŊ够č¯ģ取 Wi-Fi įŊ‘įģœįš„åį§°", "background_options": "åŽå°é€‰éĄš", "backup": "备äģŊ", - "backup_album_selection_page_albums_device": "čŽžå¤‡ä¸Šįš„į›¸å†Œīŧˆ{count}īŧ‰", + "backup_album_selection_page_albums_device": "čŽžå¤‡ä¸Šįš„į›¸į°ŋīŧˆ{count}īŧ‰", "backup_album_selection_page_albums_tap": "单å‡ģ包åĢīŧŒåŒå‡ģ排除", - "backup_album_selection_page_assets_scatter": "čĩ„æēæ–‡äģļ可čƒŊåˆ†æ•Ŗåœ¨å¤šä¸Ēį›¸å†Œä¸­ã€‚å› æ­¤īŧŒåœ¨å¤‡äģŊčŋ‡į¨‹ä¸­īŧŒæ‚¨å¯äģĨ选拊包åĢæˆ–æŽ’é™¤į‰šåŽšįš„į›¸å†Œã€‚", - "backup_album_selection_page_select_albums": "é€‰æ‹Šį›¸å†Œ", + "backup_album_selection_page_assets_scatter": "čĩ„æēå¯äģĨåˆ†æ•Ŗåœ¨å¤šä¸Ēį›¸å†Œä¸­ã€‚å› æ­¤īŧŒåœ¨å¤‡äģŊčŋ‡į¨‹ä¸­īŧŒå¯äģĨ包åĢ或排除某äē›į›¸å†Œã€‚", + "backup_album_selection_page_select_albums": "é€‰æ‹Šį›¸į°ŋ", "backup_album_selection_page_selection_info": "选拊äŋĄæ¯", - "backup_album_selection_page_total_assets": "唯一čĩ„æēæ€ģ莥", - "backup_albums_sync": "备äģŊį›¸å†ŒåŒæ­Ĩ", + "backup_album_selection_page_total_assets": "é€‰ä¸­įš„į…§į‰‡æˆ–č§†éĸ‘æ€ģ数", + "backup_albums_sync": "备äģŊᛏį°ŋ同æ­Ĩ", "backup_all": "全部", - "backup_background_service_backup_failed_message": "čĩ„æēå¤‡äģŊå¤ąč´Ĩã€‚æ­Ŗåœ¨é‡č¯•â€Ļ", - "backup_background_service_complete_notification": "čĩ„æēå¤‡äģŊ厌成", + "backup_background_service_backup_failed_message": "文äģļ备äģŊå¤ąč´Ĩã€‚æ­Ŗåœ¨é‡č¯•â€Ļ", + "backup_background_service_complete_notification": "文äģļ备äģŊ厌成", "backup_background_service_connection_failed_message": "æ— æŗ•čŋžæŽĨåˆ°æœåŠĄå™¨ã€‚æ­Ŗåœ¨é‡č¯•â€Ļ", "backup_background_service_current_upload_notification": "æ­Ŗåœ¨ä¸Šäŧ  “{filename}”", - "backup_background_service_default_notification": "æ­Ŗåœ¨æŖ€æŸĨ新čĩ„æēâ€Ļ", + "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": "åœ¨â€œčŽžįŊŽâ€>â€œé€šį”¨â€>“后台 App åˆˇæ–°â€ä¸­å¯į”¨æ­¤åŠŸčƒŊīŧŒäģĨäŊŋį”¨åŽå°å¤‡äģŊ。", "backup_controller_page_background_app_refresh_disabled_title": "后台 App åˆˇæ–°åˇ˛å…ŗé—­", "backup_controller_page_background_app_refresh_enable_button_text": "å‰åž€čŽžįŊŽ", "backup_controller_page_background_battery_info_link": "åą•į¤ē操äŊœæ­ĨéǤ", - "backup_controller_page_background_battery_info_message": "ä¸ēčŽˇåž—æœ€äŊŗįš„后台备äģŊäŊ“énjīŧŒč¯ˇåœ¨įŗģįģŸčŽžįŊŽä¸­įĻį”¨é’ˆå¯š Immich įš„äģģäŊ•į”ĩæą äŧ˜åŒ–限åˆļ。\n\nį”ąäēŽč¯Ĩ莞įŊŽå› čŽžå¤‡č€Œåŧ‚īŧŒč¯ˇæŸĨč¯ĸæ‚¨čŽžå¤‡åˆļé€ å•†įš„å…ˇäŊ“čĻæą‚ã€‚", + "backup_controller_page_background_battery_info_message": "ä¸ēčŽˇåž—æœ€äŊŗįš„后台备äģŊäŊ“énjīŧŒč¯ˇåœ¨įŗģįģŸčŽžįŊŽä¸­įĻį”¨é’ˆå¯šImmichįš„äģģäŊ•į”ĩæą äŧ˜åŒ–限åˆļ。\n\nį”ąäēŽč¯Ĩ莞įŊŽå› čŽžå¤‡č€Œåŧ‚īŧŒč¯ˇæŸĨč¯ĸæ‚¨čŽžå¤‡åˆļé€ å•†įš„å…ˇäŊ“čĻæą‚ã€‚", "backup_controller_page_background_battery_info_ok": "我įŸĨ道äē†", "backup_controller_page_background_battery_info_title": "į”ĩæą äŧ˜åŒ–", "backup_controller_page_background_charging": "äģ…在充į”ĩæ—ļ", @@ -652,7 +652,7 @@ "backup_controller_page_background_is_on": "后台č‡Ē动备äģŊ厞åŧ€å¯", "backup_controller_page_background_turn_off": "å…ŗé—­åŽå°æœåŠĄ", "backup_controller_page_background_turn_on": "åŧ€å¯åŽå°æœåŠĄ", - "backup_controller_page_background_wifi": "äģ…在 Wi-Fi 下", + "backup_controller_page_background_wifi": "äģ…在Wi-Fi下", "backup_controller_page_backup": "备äģŊ", "backup_controller_page_backup_selected": "厞选īŧš ", "backup_controller_page_backup_sub": "厞备äģŊįš„į…§į‰‡å’Œč§†éĸ‘", @@ -661,7 +661,7 @@ "backup_controller_page_excluded": "åˇ˛æŽ’é™¤īŧš ", "backup_controller_page_failed": "å¤ąč´Ĩīŧˆ{count}īŧ‰", "backup_controller_page_filename": "文äģļ名īŧš{filename} [{size}]", - "backup_controller_page_id": "IDīŧš{id}", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "备äģŊäŋĄæ¯", "backup_controller_page_none_selected": "暂æœĒ选拊", "backup_controller_page_remainder": "削äŊ™", @@ -671,14 +671,14 @@ "backup_controller_page_status_off": "æœĒåŧ€å¯å‰å°č‡Ē动备äģŊ", "backup_controller_page_status_on": "前台č‡Ē动备äģŊåˇ˛æ‰“åŧ€", "backup_controller_page_storage_format": "厞ᔍ {used}īŧˆå…ą {total}īŧ‰", - "backup_controller_page_to_backup": "垅备äģŊįš„į›¸å†Œ", - "backup_controller_page_total_sub": "包åĢæ‰€é€‰į›¸å†Œå†…å…¨éƒ¨å”¯ä¸€įš„į…§į‰‡å’Œč§†éĸ‘", + "backup_controller_page_to_backup": "垅备äģŊįš„į›¸į°ŋ", + "backup_controller_page_total_sub": "包åĢæ‰€é€‰į›¸į°ŋå†…å…¨éƒ¨å”¯ä¸€įš„į…§į‰‡å’Œč§†éĸ‘", "backup_controller_page_turn_off": "å…ŗé—­å‰å°å¤‡äģŊ", "backup_controller_page_turn_on": "åŧ€å¯å‰å°å¤‡äģŊ", "backup_controller_page_uploading_file_info": "æ­Ŗåœ¨ä¸Šäŧ æ–‡äģļäŋĄæ¯", - "backup_err_only_album": "æ— æŗ•åˆ é™¤å”¯ä¸€įš„į›¸å†Œ", + "backup_err_only_album": "æ— æŗ•åˆ é™¤å”¯ä¸€įš„į›¸į°ŋ", "backup_error_sync_failed": "同æ­Ĩå¤ąč´Ĩã€‚æ— æŗ•å¤„į†å¤‡äģŊ。", - "backup_info_card_assets": "čĩ„äē§", + "backup_info_card_assets": "į…§į‰‡å’Œč§†éĸ‘", "backup_manual_cancelled": "åˇ˛å–æļˆ", "backup_manual_in_progress": "上äŧ æ­Ŗåœ¨čŋ›čĄŒä¸­īŧŒč¯ˇį¨åŽå†č¯•", "backup_manual_success": "成功", @@ -707,10 +707,10 @@ "cache_settings_clear_cache_button_title": "æ¸…į†åē”ᔍįŧ“存。在įŧ“存重åģ翜Ÿé—´īŧŒåē”į”¨įš„čŋčĄŒé€ŸåēĻäŧšæ˜Žæ˜žå˜æ…ĸ。", "cache_settings_duplicated_assets_clear_button": "清除", "cache_settings_duplicated_assets_subtitle": "åŋŊį•Ĩåˆ—čĄ¨ä¸­įš„åĒ’äŊ“æ–‡äģļ", - "cache_settings_duplicated_assets_title": "重复čĩ„äē§īŧˆ{count}īŧ‰", + "cache_settings_duplicated_assets_title": "重复文äģļīŧˆ{count}īŧ‰", "cache_settings_statistics_album": "回åē“įŧŠį•Ĩ回", "cache_settings_statistics_full": "原回", - "cache_settings_statistics_shared": "å…ąäēĢį›¸å†ŒįŧŠį•Ĩ回", + "cache_settings_statistics_shared": "å…ąäēĢᛏį°ŋįŧŠį•Ĩ回", "cache_settings_statistics_thumbnail": "įŧŠį•Ĩ回", "cache_settings_statistics_title": "įŧ“å­˜å į”¨æƒ…å†ĩ", "cache_settings_subtitle": "įŽĄį† Immich 手æœēįĢ¯įš„įŧ“å­˜", @@ -752,25 +752,25 @@ "changed_visibility_successfully": "å¯č§įŠļ态更新成功", "charging": "充į”ĩ中", "charging_requirement_mobile_backup": "后台备äģŊ需čĻčŽžå¤‡å¤„äēŽå……į”ĩįŠļ态", - "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": "æŖ€æŸĨæ—Ĩåŋ—", "checksum": "æ ĄéĒŒå’Œ", "choose_matching_people_to_merge": "选拊čρ合åšļįš„äēēį‰Š", "city": "城市", - "cleanup_confirm_description": "Immich åˇ˛æ‰žåˆ° {count} ä¸Ē厉全备äģŊč‡ŗæœåŠĄå™¨įš„čĩ„æēīŧˆåˆ›åģēäēŽ {date} 䚋前īŧ‰ã€‚是åĻäģŽæ­¤čŽžå¤‡į§ģ除æœŦ地副æœŦīŧŸ", + "cleanup_confirm_description": "Immichåˇ˛æ‰žåˆ°{count}ä¸Ē厉全备äģŊč‡ŗæœåŠĄå™¨įš„æ–‡äģļīŧˆåˆ›åģēäēŽ{date}䚋前īŧ‰ã€‚是åĻäģŽæ­¤čŽžå¤‡į§ģ除æœŦ地副æœŦīŧŸ", "cleanup_confirm_prompt_title": "是åĻäģŽæ­¤čŽžå¤‡į§ģ除īŧŸ", - "cleanup_deleted_assets": "厞将 {count} ä¸Ēčĩ„æēį§ģč‡ŗčŽžå¤‡å›žæ”ļįĢ™", + "cleanup_deleted_assets": "厞将{count}ä¸Ē文äģļį§ģč‡ŗčŽžå¤‡å›žæ”ļįĢ™", "cleanup_deleting": "æ­Ŗåœ¨į§ģč‡ŗå›žæ”ļįĢ™...", - "cleanup_found_assets": "åˇ˛æ‰žåˆ° {count} ä¸Ē厞备äģŊįš„čĩ„æē", - "cleanup_found_assets_with_size": "åˇ˛æ‰žåˆ° {count} ä¸Ē厞备äģŊįš„čĩ„æē ({size})", - "cleanup_icloud_shared_albums_excluded": "iCloud å…ąäēĢį›¸å†Œåˇ˛æŽ’é™¤åœ¨æ‰ĢæčŒƒå›´äš‹å¤–", - "cleanup_no_assets_found": "æœĒ扞到įŦĻ合上čŋ°æĄäģļįš„čĩ„æēã€‚“释攞įŠē间”äģ…čƒŊį§ģ除厞备äģŊč‡ŗæœåŠĄå™¨įš„æ–‡äģļ", - "cleanup_preview_title": "åž…į§ģé™¤įš„čĩ„æē ({count})", - "cleanup_step3_description": "æ‰Ģ描įŦĻ合æ—Ĩ期及äŋį•™čŽžįŊŽįš„厞备äģŊčĩ„æēã€‚", - "cleanup_step4_summary": "将äģŽæœŦæœēį§ģ除 {count} ä¸Ēčĩ„æēīŧˆåˆ›åģēäēŽ {date} 䚋前īŧ‰ã€‚ᅧቇäģå¯åœ¨ Immich åē”ᔍ䏭čŽŋ闎。", - "cleanup_trash_hint": "ä¸ēåŊģåē•释攞存储įŠē间īŧŒč¯ˇæ‰“åŧ€įŗģįģŸį›¸å†Œåē”ᔍåšļ清įŠē回æ”ļįĢ™", + "cleanup_found_assets": "åˇ˛æ‰žåˆ°{count}ä¸Ē厞备äģŊįš„æ–‡äģļ", + "cleanup_found_assets_with_size": "åˇ˛æ‰žåˆ°{count}ä¸Ē厞备äģŊįš„æ–‡äģļīŧˆ{size}īŧ‰", + "cleanup_icloud_shared_albums_excluded": "iCloudå…ąäēĢᛏį°ŋåˇ˛æŽ’é™¤åœ¨æ‰ĢæčŒƒå›´äš‹å¤–", + "cleanup_no_assets_found": "æœĒ扞到įŦĻ合上čŋ°æĄäģļįš„æ–‡äģļ。“释攞įŠē间”äģ…čƒŊį§ģ除厞备äģŊč‡ŗæœåŠĄå™¨įš„æ–‡äģļ", + "cleanup_preview_title": "åž…į§ģé™¤įš„į…§į‰‡/视éĸ‘īŧˆ{count}īŧ‰", + "cleanup_step3_description": "æ‰Ģ描įŦĻ合æ—Ĩ期及äŋį•™čŽžįŊŽįš„厞备äģŊᅧቇ/视éĸ‘。", + "cleanup_step4_summary": "将äģŽæœŦ地į§ģ除{count}ä¸Ēᅧቇ/视éĸ‘īŧˆåˆ›åģēäēŽ {date} 䚋前īŧ‰ã€‚ᅧቇäģå¯åœ¨Immich App中čŽŋ闎。", + "cleanup_trash_hint": "ä¸ēåŊģåē•释攞存储įŠē间īŧŒč¯ˇæ‰“åŧ€įŗģįģŸį…§į‰‡Appåšļ清įŠē回æ”ļįĢ™", "clear": "清įŠē", "clear_all": "全部清除", "clear_all_recent_searches": "清除全部最čŋ‘搜į´ĸ莰åŊ•", @@ -786,7 +786,7 @@ "client_cert_password_title": "蝁äšĻ坆᠁", "client_cert_remove_msg": "åŽĸæˆˇį̝蝁äšĻ厞į§ģ除", "client_cert_subtitle": "äģ…æ”¯æŒ PKCS12 æ ŧåŧ (.p12, .pfx)。į™ģåŊ•åŽå°†æ— æŗ•å¯ŧå…Ĩ或į§ģ除蝁äšĻ", - "client_cert_title": "SSL åŽĸæˆˇį̝蝁äšĻ[厞éĒŒæ€§åŠŸčƒŊ]", + "client_cert_title": "SSLåŽĸæˆˇį̝蝁äšĻ [厞éĒŒæ€§åŠŸčƒŊ]", "clockwise": "éĄēæ—ļ针", "close": "å…ŗé—­", "collapse": "æ”ļčĩˇ", @@ -803,13 +803,13 @@ "comment_options": "更多", "comments_and_likes": "蝄čŽē & į‚ščĩž", "comments_are_disabled": "蝄čŽē厞兺闭", - "common_create_new_album": "创åģēæ–°į›¸å†Œ", + "common_create_new_album": "创åģēæ–°į›¸į°ŋ", "completed": "åˇ˛åŽŒæˆ", "confirm": "įĄŽčŽ¤", "confirm_admin_password": "įĄŽčŽ¤įŽĄį†å‘˜å¯†į ", "confirm_delete_face": "įĄŽåŽščρäģŽæ­¤æ–‡äģļ中删除 {name} įš„éĸ部äŋĄæ¯å—īŧŸ", "confirm_delete_shared_link": "įĄŽåŽščĻåˆ é™¤æ­¤å…ąäēĢ链æŽĨ吗īŧŸ", - "confirm_keep_this_delete_others": "堆栈中所有å…ļäģ–čĩ„æēéƒŊ将čĸĢ删除īŧŒäģ…äŋį•™æ­¤čĩ„æēã€‚įĄŽåޚčρįģ§įģ­å—īŧŸ", + "confirm_keep_this_delete_others": "å †å ä¸­é™¤æ­¤éĄšį›Žå¤–įš„æ‰€æœ‰å…ļäģ–éĄšį›ŽéƒŊ将čĸĢåˆ é™¤ã€‚įĄŽåŽščρįģ§įģ­å—īŧŸ", "confirm_new_pin_code": "įĄŽčŽ¤æ–° PIN ᠁", "confirm_password": "įĄŽčŽ¤å¯†į ", "confirm_tag_face": "是åĻ将此äēēč„¸æ ‡čŽ°ä¸ē {name}īŧŸ", @@ -819,8 +819,8 @@ "contain": "适åē”", "context": "äģĨ文搜回", "continue": "įģ§įģ­", - "control_bottom_app_bar_create_new_album": "新åģēį›¸å†Œ", - "control_bottom_app_bar_delete_from_immich": "äģŽ Immich æœåŠĄå™¨ä¸­åˆ é™¤", + "control_bottom_app_bar_create_new_album": "新åģēᛏį°ŋ", + "control_bottom_app_bar_delete_from_immich": "äģŽImmichæœåŠĄå™¨ä¸­åˆ é™¤", "control_bottom_app_bar_delete_from_local": "äģŽčŽžå¤‡ä¸­åˆ é™¤", "control_bottom_app_bar_edit_location": "įŧ–čž‘äŊįŊŽ", "control_bottom_app_bar_edit_time": "įŧ–čž‘æ—Ĩ期和æ—ļ间", @@ -840,19 +840,22 @@ "cover": "åĄĢ充", "covers": "封éĸ", "create": "创åģē", - "create_album": "创åģēį›¸å†Œ", + "create_album": "创åģēᛏį°ŋ", "create_album_page_untitled": "æœĒå‘Ŋ名", "create_api_key": "创åģē API 密é’Ĩ", "create_first_workflow": "创åģēéĻ–ä¸ĒåˇĨäŊœæĩ", - "create_library": "创åģēčĩ„æ–™åē“", + "create_library": "创åģēčĩ„æēåē“", "create_link": "创åģē链æŽĨ", "create_link_to_share": "创åģēå…ąäēĢ链æŽĨ", "create_link_to_share_description": "å…čŽ¸äģģäŊ•æ‹Ĩ有铞æŽĨįš„äē翟Ĩįœ‹æ‰€é€‰į…§į‰‡", "create_new": "新åģē", + "create_new_face": "创åģēæ–°äēē脸", "create_new_person": "创åģēæ–°äēēį‰Š", - "create_new_person_hint": "将所选čĩ„æēåˆ†é…į왿–°äēēį‰Š", + "create_new_person_hint": "å°†æ‰€é€‰į…§į‰‡/视éĸ‘分配į왿–°äēēį‰Š", "create_new_user": "新åģēį”¨æˆˇ", - "create_shared_album_page_share_add_assets": "æˇģ加čĩ„æē", + "create_person": "创åģēäēēį‰Š", + "create_person_subtitle": "ä¸ē所选äēē脸æˇģ加姓名īŧŒäģĨ创åģēåšļæ ‡čŽ°æ–°äēēį‰Š", + "create_shared_album_page_share_add_assets": "æˇģåŠ į…§į‰‡/视éĸ‘", "create_shared_album_page_share_select_photos": "é€‰æ‹Šį…§į‰‡", "create_shared_link": "创åģēå…ąäēĢ链æŽĨ", "create_tag": "创åģēæ ‡į­ž", @@ -861,11 +864,12 @@ "create_workflow": "新åģēåˇĨäŊœæĩ", "created": "åˇ˛åˆ›åģē", "created_at": "创åģēæ—ļ间", - "creating_linked_albums": "æ­Ŗåœ¨åˆ›åģēį›¸å†Œé“žæŽĨâ€Ļ", + "creating_linked_albums": "æ­Ŗåœ¨åˆ›åģēᛏį°ŋ链æŽĨâ€Ļ", "crop": "誁å‰Ē", "crop_aspect_ratio_fixed": "å›ē厚比䞋", "crop_aspect_ratio_free": "č‡Ēį”ąæ¯”äž‹", "crop_aspect_ratio_original": "原始比䞋", + "crop_aspect_ratio_square": "æ–šåŊĸ", "curated_object_page_title": "į˛žé€‰é›†", "current_device": "åŊ“å‰čŽžå¤‡", "current_pin_code": "åŊ“前 PIN ᠁", @@ -880,7 +884,7 @@ "daily_title_text_date": "MMM dd (E)", "daily_title_text_date_year": "YYYYåš´M月dæ—Ĩ (E)", "dark": "æˇąč‰˛", - "dark_theme": "切æĸæˇąč‰˛ä¸ģéĸ˜", + "dark_theme": "切æĸåˆ°æˇąč‰˛ä¸ģéĸ˜", "date": "æ—Ĩ期", "date_after": "åŧ€å§‹æ—Ĩ期", "date_and_time": "æ—Ĩ期与æ—ļ间", @@ -891,14 +895,12 @@ "day": "æ—Ĩ", "days": "夊", "deduplicate_all": "åˆ é™¤æ‰€æœ‰é‡å¤éĄš", - "deduplication_criteria_1": "回像大小īŧˆå­—节īŧ‰", - "deduplication_criteria_2": "EXIF æ•°æŽčŽĄæ•°", - "deduplication_info": "åŽģ重įģŸčŽĄ", - "deduplication_info_description": "ä¸ēäē†č‡Ē动éĸ„é€‰į´ æåšļ扚量åŽģé™¤é‡å¤éĄšīŧŒæˆ‘äģŦäŧšå‚č€ƒäģĨ下äŋĄæ¯īŧš", + "default_locale": "éģ˜čޤ蝭荀", + "default_locale_description": "æ šæŽæ‚¨įš„æĩč§ˆå™¨åŒē域æ ŧåŧåŒ–æ—Ĩ期和数字", "delete": "删除", "delete_action_confirmation_message": "æ‚¨įĄŽåŽščĻåˆ é™¤æ­¤į´ æå—īŧŸæ­¤æ“äŊœäŧšå°†č¯Ĩį´ æį§ģč‡ŗæœåŠĄå™¨įš„å›žæ”ļįĢ™īŧŒåšļ提į¤ē您是åĻ将å…ļ在æœŦåœ°čŽžå¤‡ä¸Šåˆ é™¤", "delete_action_prompt": "åˇ˛åˆ é™¤ {count} 饚", - "delete_album": "åˆ é™¤į›¸å†Œ", + "delete_album": "åˆ é™¤į›¸į°ŋ", "delete_api_key_prompt": "æ‚¨įĄŽåŽščĻåˆ é™¤æ­¤ API 密é’Ĩ吗īŧŸ", "delete_dialog_alert": "čŋ™äē›éĄšį›Žå°†äģŽ Immich æœåŠĄå™¨äģĨ及äŊ įš„čŽžå¤‡ä¸ŠčĸĢæ°¸äš…删除", "delete_dialog_alert_local": "čŋ™äē›éĄšį›Žå°†äģŽäŊ įš„čŽžå¤‡ä¸ŠčĸĢæ°¸äš…į§ģ除īŧŒäŊ†äžį„ļäŧšäŋį•™åœ¨ Immich æœåŠĄå™¨ä¸Š", @@ -909,14 +911,14 @@ "delete_duplicates_confirmation": "æ‚¨įĄŽåŽščĻæ°¸äš…åˆ é™¤čŋ™äē›é‡å¤éĄšå—īŧŸ", "delete_face": "删除č¯Ĩäēē脸", "delete_key": "删除密é’Ĩ", - "delete_library": "删除čĩ„æ–™åē“", + "delete_library": "删除čĩ„æēåē“", "delete_link": "删除铞æŽĨ", - "delete_local_action_prompt": "{count} éĄšåˇ˛åœ¨æœŦ地删除", + "delete_local_action_prompt": "{count}éĄšåˇ˛åœ¨æœŦ地删除", "delete_local_dialog_ok_backed_up_only": "äģ…åˆ é™¤åˇ˛å¤‡äģŊįš„éĄšį›Ž", "delete_local_dialog_ok_force": "åŧēåˆļ删除", - "delete_others": "删除å…ļ厃", + "delete_others": "删除å…ļäģ–", "delete_permanently": "永䚅删除", - "delete_permanently_action_prompt": "{count} éĄšåˇ˛æ°¸äš…åˆ é™¤", + "delete_permanently_action_prompt": "{count}éĄšåˇ˛æ°¸äš…åˆ é™¤", "delete_shared_link": "åˆ é™¤å…ąäēĢ链æŽĨ", "delete_shared_link_dialog_title": "åˆ é™¤å…ąäēĢ链æŽĨ", "delete_tag": "åˆ é™¤æ ‡į­ž", @@ -933,7 +935,7 @@ "disable": "įρᔍ", "disabled": "įρᔍ", "disallow_edits": "įρæ­ĸįŧ–čž‘", - "discord": "Discord į¤žåŒē", + "discord": "Discord", "discover": "å‘įŽ°", "discovered_devices": "åˇ˛å‘įŽ°įš„čŽžå¤‡", "dismiss_all_errors": "åŋŊį•Ĩæ‰€æœ‰é”™č¯¯", @@ -970,10 +972,10 @@ "downloading_media": "æ­Ŗåœ¨ä¸‹čŊŊåĒ’äŊ“æ–‡äģļ", "drop_files_to_upload": "随意拖攞文äģļäģĨ上äŧ ", "duplicates": "é‡å¤éĄš", - "duplicates_description": "č¯ˇé€ä¸€æ ‡čŽ°æ¯įģ„ä¸­įš„é‡å¤æ–‡äģļ", + "duplicates_description": "č¯ˇé€ä¸€æ ‡čŽ°æ¯įģ„ä¸­įš„é‡å¤æ–‡äģļ。", "duration": "æ—ļé•ŋ", "edit": "įŧ–čž‘", - "edit_album": "įŧ–čž‘į›¸å†Œ", + "edit_album": "įŧ–čž‘į›¸į°ŋ", "edit_avatar": "įŧ–čž‘å¤´åƒ", "edit_birthday": "įŧ–čž‘į”Ÿæ—Ĩ", "edit_date": "įŧ–čž‘æ—Ĩ期", @@ -1007,7 +1009,7 @@ "editor_edits_applied_success": "įŧ–čž‘åˇ˛æˆåŠŸåē”ᔍ", "editor_flip_horizontal": "æ°´åšŗįŋģčŊŦ", "editor_flip_vertical": "åž‚į›´įŋģčŊŦ", - "editor_handle_corner": "{corner, select, top_left {åˇĻ上角} top_right {åŗä¸Šč§’} bottom_left {åˇĻ下角} bottom_right {åŗä¸‹č§’} other {某ä¸Ē}} 角čŊįš„æŽ§åˆļ手柄", + "editor_handle_corner": "{corner, select, top_left {åˇĻ上角} top_right {åŗä¸Šč§’} bottom_left {åˇĻ下角} bottom_right {åŗä¸‹č§’} other {某ä¸Ē}}角čŊįš„æŽ§åˆļ手柄", "editor_handle_edge": "{edge, select, top {éĄļ部} bottom {åē•部} left {åˇĻäž§} right {åŗäž§} other {某ä¸Ē}} čžšįŧ˜įš„æŽ§åˆļ手柄", "editor_orientation": "斚向", "editor_reset_all_changes": "čŋ˜åŽŸæ›´æ”š", @@ -1017,7 +1019,7 @@ "email_notifications": "邮äģļ通įŸĨ", "empty_folder": "此文äģļ多ä¸ēįŠē", "empty_trash": "清įŠē回æ”ļįĢ™", - "empty_trash_confirmation": "įĄŽåŽščĻæ¸…įŠē回æ”ļį̙吗īŧŸæ­¤æ“äŊœå°†æ°¸äš…删除回æ”ļįĢ™ä¸­įš„æ‰€æœ‰čĩ„æēã€‚\nč¯Ĩ操äŊœæ— æŗ•撤销īŧ", + "empty_trash_confirmation": "įĄŽåŽščĻæ¸…įŠē回æ”ļį̙吗īŧŸæ­¤æ“äŊœå°†æ°¸äš…删除回æ”ļįĢ™ä¸­įš„æ‰€æœ‰į…§į‰‡/视éĸ‘。\nč¯Ĩ操äŊœæ— æŗ•撤销īŧ", "enable": "吝ᔍ", "enable_backup": "吝ᔍ备äģŊ", "enable_biometric_auth_description": "č¯ˇčž“å…Ĩæ‚¨įš„ PIN ᠁äģĨå¯į”¨į”Ÿį‰Šč¯†åˆĢčŽ¤č¯", @@ -1028,49 +1030,49 @@ "enter_your_pin_code": "输å…Ĩæ‚¨įš„ PIN ᠁", "enter_your_pin_code_subtitle": "输å…ĨäŊ įš„ PIN ᠁äģĨčŽŋé—Žé”åŽšįš„æ–‡äģļ多", "error": "错蝝", - "error_change_sort_album": "æ›´æ”šį›¸å†ŒæŽ’åēéĄēåēå¤ąč´Ĩ", - "error_delete_face": "删除č¯Ĩčĩ„äē§ä¸­įš„äēē脸æ—ļå‡ē错", + "error_change_sort_album": "æ›´æ”šį›¸į°ŋ排åēéĄēåēå¤ąč´Ĩ", + "error_delete_face": "删除č¯Ĩᅧቇ/视éĸ‘ä¸­įš„äēē脸æ—ļå‡ē错", "error_getting_places": "čŽˇå–åœ°į‚šäŋĄæ¯æ—ļå‡ē错", - "error_loading_albums": "加čŊŊį›¸å†Œæ—ļå‡ē错", + "error_loading_albums": "加čŊŊᛏį°ŋæ—ļå‡ē错", "error_loading_image": "加čŊŊå›žį‰‡æ—ļå‡ē错", "error_loading_partners": "加čŊŊ协äŊœč€…æ—ļå‡ē错īŧš{error}", - "error_retrieving_asset_information": "čŽˇå–čĩ„æēäŋĄæ¯æ—ļå‡ē错", + "error_retrieving_asset_information": "čŽˇå–éĄšį›ŽäŋĄæ¯æ—ļå‡ē错", "error_saving_image": "错蝝īŧš{error}", "error_tag_face_bounding_box": "æ ‡čŽ°äēē脸æ—ļå‡ē错 - æ— æŗ•čŽˇå–čžšį•ŒæĄ†åæ ‡", "error_title": "错蝝 - å‡ēįŽ°äē†é—Žéĸ˜", - "error_while_navigating": "莺čŊŦ到čĩ„æēæ—ļå‡ē错", + "error_while_navigating": "莺čŊŦåˆ°į…§į‰‡/视éĸ‘æ—ļå‡ē错", "errors": { - "cannot_navigate_next_asset": "æ— æŗ•čˇŗčŊŦ到下一ä¸Ēčĩ„æē", - "cannot_navigate_previous_asset": "æ— æŗ•čˇŗčŊŦ到上一ä¸Ēčĩ„æē", + "cannot_navigate_next_asset": "æ— æŗ•čˇŗčŊŦ到下一ä¸Ēᅧቇ/视éĸ‘", + "cannot_navigate_previous_asset": "æ— æŗ•čˇŗčŊŦ到上一ä¸Ēᅧቇ/视éĸ‘", "cant_apply_changes": "æ— æŗ•åē”į”¨æ›´æ”š", "cant_change_activity": "æ— æŗ• {enabled, select, true {įρᔍ} other {吝ᔍ}} æ´ģ动", - "cant_change_asset_favorite": "æ— æŗ•æ›´æ”ščĩ„æēįš„æ”ļ藏įŠļ态", - "cant_change_metadata_assets_count": "æ— æŗ•äŋŽæ”š{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}įš„å…ƒæ•°æŽ", + "cant_change_asset_favorite": "æ— æŗ•æ›´æ”šį…§į‰‡/视éĸ‘įš„æ”ļ藏įŠļ态", + "cant_change_metadata_assets_count": "æ— æŗ•äŋŽæ”š{count, plural, one {#ä¸Ē} other {#ä¸Ē}}ᅧቇ/视éĸ‘įš„å…ƒæ•°æŽ", "cant_get_faces": "æ— æŗ•čŽˇå–äēē脸", "cant_get_number_of_comments": "æ— æŗ•čŽˇå–č¯„čŽē数量", "cant_search_people": "æ— æŗ•æœį´ĸäēēį‰Š", "cant_search_places": "æ— æŗ•æœį´ĸåœ°į‚š", - "error_adding_assets_to_album": "æˇģ加čĩ„æēåˆ°į›¸å†Œæ—ļå‡ē错", - "error_adding_users_to_album": "æˇģåŠ į”¨æˆˇåˆ°į›¸å†Œæ—ļå‡ē错", + "error_adding_assets_to_album": "æˇģåŠ į…§į‰‡/视éĸ‘åˆ°į›¸į°ŋæ—ļå‡ē错", + "error_adding_users_to_album": "æˇģåŠ į”¨æˆˇåˆ°į›¸į°ŋæ—ļå‡ē错", "error_deleting_shared_user": "åˆ é™¤å…ąäēĢį”¨æˆˇæ—ļå‡ē错", - "error_downloading": "下čŊŊ“{filename}”æ—ļå‡ē错", + "error_downloading": "下čŊŊ{filename}æ—ļå‡ē错", "error_hiding_buy_button": "éšč—č´­äš°æŒ‰é’Žæ—ļå‡ē错", - "error_removing_assets_from_album": "į§ģé™¤į›¸å†Œčĩ„æēæ—ļå‡ē错īŧŒč¯ˇæŖ€æŸĨ控åˆļ台äģĨčŽˇå–æ›´å¤šč¯Ļ情", - "error_selecting_all_assets": "全选čĩ„æēæ—ļå‡ē错", + "error_removing_assets_from_album": "äģŽį›¸į°ŋ中į§ģ除ᅧቇ/视éĸ‘æ—ļå‡ē错īŧŒč¯ˇæŖ€æŸĨ控åˆļ台äģĨčŽˇå–æ›´å¤šč¯Ļ情", + "error_selecting_all_assets": "免选ᅧቇ/视éĸ‘æ—ļå‡ē错", "exclusion_pattern_already_exists": "æ­¤æŽ’é™¤æ¨Ąåŧåˇ˛å­˜åœ¨ã€‚", - "failed_to_create_album": "创åģēį›¸å†Œå¤ąč´Ĩ", + "failed_to_create_album": "创åģēᛏį°ŋå¤ąč´Ĩ", "failed_to_create_shared_link": "创åģēå…ąäēĢ链æŽĨå¤ąč´Ĩ", "failed_to_edit_shared_link": "įŧ–čž‘å…ąäēĢ链æŽĨå¤ąč´Ĩ", "failed_to_get_people": "čŽˇå–äēēį‰Šåˆ—čĄ¨å¤ąč´Ĩ", - "failed_to_keep_this_delete_others": "äŋį•™æ­¤čĩ„æēåšļ删除å…ļäģ–čĩ„æēå¤ąč´Ĩ", - "failed_to_load_asset": "加čŊŊčĩ„æēå¤ąč´Ĩ", - "failed_to_load_assets": "加čŊŊčĩ„æēå¤ąč´Ĩ", + "failed_to_keep_this_delete_others": "åˆ é™¤é™¤æ­¤éĄšį›Žå¤–įš„å…ļäģ–éĄšį›Žå¤ąč´Ĩ", + "failed_to_load_asset": "加čŊŊᅧቇ/视éĸ‘å¤ąč´Ĩ", + "failed_to_load_assets": "加čŊŊᅧቇ/视éĸ‘å¤ąč´Ĩ", "failed_to_load_notifications": "加čŊŊ通įŸĨå¤ąč´Ĩ", "failed_to_load_people": "加čŊŊäēēį‰Šå¤ąč´Ĩ", "failed_to_remove_product_key": "į§ģ除äē§å“å¯†é’Ĩå¤ąč´Ĩ", "failed_to_reset_pin_code": "重įŊŽ PIN į å¤ąč´Ĩ", - "failed_to_stack_assets": "堆叠čĩ„æēå¤ąč´Ĩ", - "failed_to_unstack_assets": "取æļˆå †å čĩ„æēå¤ąč´Ĩ", + "failed_to_stack_assets": "å †å į…§į‰‡/视éĸ‘å¤ąč´Ĩ", + "failed_to_unstack_assets": "取æļˆå †å į…§į‰‡/视éĸ‘å¤ąč´Ĩ", "failed_to_update_notification_status": "更新通įŸĨįŠļæ€å¤ąč´Ĩ", "incorrect_email_or_password": "é‚ŽįŽąæˆ–å¯†į é”™č¯¯", "library_folder_already_exists": "č¯Ĩå¯ŧå…Ĩčˇ¯åž„åˇ˛å­˜åœ¨ã€‚", @@ -1079,18 +1081,18 @@ "profile_picture_transparent_pixels": "å¤´åƒä¸æ”¯æŒé€æ˜ŽčƒŒæ™¯īŧŒč¯ˇæ”žå¤§æˆ–į§ģåŠ¨å›žį‰‡ã€‚", "quota_higher_than_disk_size": "äŊ čŽžįŊŽįš„配éĸčļ…čŋ‡äē†įŖį›˜æ€ģ大小", "something_went_wrong": "å‡ē错äē†", - "unable_to_add_album_users": "æ— æŗ•å‘į›¸å†ŒæˇģåŠ į”¨æˆˇ", - "unable_to_add_assets_to_shared_link": "æ— æŗ•å‘åˆ†äēĢ链æŽĨæˇģ加čĩ„æē", + "unable_to_add_album_users": "æ— æŗ•å‘į›¸į°ŋæˇģåŠ į”¨æˆˇ", + "unable_to_add_assets_to_shared_link": "æ— æŗ•å‘åˆ†äēĢ链æŽĨæˇģåŠ į…§į‰‡/视éĸ‘", "unable_to_add_comment": "æ— æŗ•æˇģåŠ č¯„čŽē", "unable_to_add_exclusion_pattern": "æ— æŗ•æˇģåŠ æŽ’é™¤č§„åˆ™", "unable_to_add_partners": "æ— æŗ•æˇģ加协äŊœč€…", - "unable_to_add_remove_archive": "æ— æŗ•{archived, select, true {äģŽåŊ’æĄŖä¸­į§ģ除} other {æˇģ加čĩ„æēåˆ°åŊ’æĄŖ}}", - "unable_to_add_remove_favorites": "æ— æŗ•{favorite, select, true {æˇģ加čĩ„æēåˆ°æ”ļ藏} other {äģŽæ”ļ藏中į§ģ除}}", + "unable_to_add_remove_archive": "æ— æŗ•{archived, select, true {äģŽåŊ’æĄŖä¸­į§ģ除} other {æˇģåŠ į…§į‰‡/视éĸ‘到åŊ’æĄŖ}}", + "unable_to_add_remove_favorites": "æ— æŗ•{favorite, select, true {æˇģåŠ į…§į‰‡/视éĸ‘到æ”ļ藏} other {äģŽæ”ļ藏中į§ģ除}}", "unable_to_archive_unarchive": "æ— æŗ•{archived, select, true {åŊ’æĄŖ} other {取æļˆåŊ’æĄŖ}}", - "unable_to_change_album_user_role": "æ— æŗ•æ›´æ”šį›¸å†Œį”¨æˆˇįš„č§’č‰˛", + "unable_to_change_album_user_role": "æ— æŗ•æ›´æ”šį›¸į°ŋį”¨æˆˇįš„č§’č‰˛", "unable_to_change_date": "æ— æŗ•æ›´æ”šæ—Ĩ期", "unable_to_change_description": "æ— æŗ•äŋŽæ”šæčŋ°", - "unable_to_change_favorite": "æ— æŗ•æ›´æ”ščĩ„æēįš„æ”ļ藏įŠļ态", + "unable_to_change_favorite": "æ— æŗ•æ›´æ”šį…§į‰‡/视éĸ‘įš„æ”ļ藏įŠļ态", "unable_to_change_location": "æ— æŗ•æ›´æ”šäŊįŊŽ", "unable_to_change_password": "æ— æŗ•äŋŽæ”šå¯†į ", "unable_to_change_visibility": "æ— æŗ•äŋŽæ”š{count, plural, one {#ä¸Ēäēē} other {#ä¸Ēäēē}}įš„å¯č§æ€§čŽžįŊŽ", @@ -1102,9 +1104,9 @@ "unable_to_create_api_key": "æ— æŗ•åˆ›åģēæ–°įš„ API 密é’Ĩ", "unable_to_create_library": "æ— æŗ•åˆ›åģēåē“", "unable_to_create_user": "æ— æŗ•åˆ›åģēį”¨æˆˇ", - "unable_to_delete_album": "æ— æŗ•åˆ é™¤į›¸å†Œ", - "unable_to_delete_asset": "æ— æŗ•åˆ é™¤čĩ„æē", - "unable_to_delete_assets": "删除čĩ„æē æ—ļå‡ē错", + "unable_to_delete_album": "æ— æŗ•åˆ é™¤į›¸į°ŋ", + "unable_to_delete_asset": "æ— æŗ•åˆ é™¤į…§į‰‡/视éĸ‘", + "unable_to_delete_assets": "åˆ é™¤į…§į‰‡/视éĸ‘æ—ļå‡ē错", "unable_to_delete_exclusion_pattern": "æ— æŗ•åˆ é™¤æŽ’é™¤č§„åˆ™", "unable_to_delete_shared_link": "æ— æŗ•åˆ é™¤å…ąäēĢ链æŽĨ", "unable_to_delete_user": "æ— æŗ•åˆ é™¤į”¨æˆˇ", @@ -1124,21 +1126,21 @@ "unable_to_login_with_oauth": "æ— æŗ•äŊŋᔍ OAuth čŋ›čĄŒį™ģåŊ•", "unable_to_play_video": "æ— æŗ•æ’­æ”žč§†éĸ‘", "unable_to_reassign_assets_existing_person": "æ— æŗ•å°†éĄšį›Žé‡æ–°åˆ†é…įģ™{name, select, null {åˇ˛å­˜åœ¨įš„äēēį‰Š} other {{name}}}", - "unable_to_reassign_assets_new_person": "æ— æŗ•é‡æ–°åˆ†é…čĩ„æēį왿–°įš„äēēį‰Š", + "unable_to_reassign_assets_new_person": "æ— æŗ•é‡æ–°åˆ†é…į…§į‰‡/视éĸ‘į왿–°įš„äēēį‰Š", "unable_to_refresh_user": "æ— æŗ•åˆˇæ–°į”¨æˆˇ", - "unable_to_remove_album_users": "æ— æŗ•äģŽį›¸å†Œä¸­į§ģé™¤į”¨æˆˇ", + "unable_to_remove_album_users": "æ— æŗ•äģŽį›¸į°ŋ中į§ģé™¤į”¨æˆˇ", "unable_to_remove_api_key": "æ— æŗ•į§ģ除 API 密é’Ĩ", - "unable_to_remove_assets_from_shared_link": "æ— æŗ•äģŽå…ąäēĢ链æŽĨ中į§ģ除čĩ„æē", + "unable_to_remove_assets_from_shared_link": "æ— æŗ•äģŽå…ąäēĢ链æŽĨ中į§ģ除ᅧቇ/视éĸ‘", "unable_to_remove_library": "æ— æŗ•į§ģ除åē“", "unable_to_remove_partner": "æ— æŗ•į§ģ除协äŊœč€…", "unable_to_remove_reaction": "æ— æŗ•åˆ é™¤å›žå¤", "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": "æ— æŗ•äŋå­˜į›¸å†Œ", + "unable_to_save_album": "æ— æŗ•äŋå­˜į›¸į°ŋ", "unable_to_save_api_key": "æ— æŗ•äŋå­˜ API 密é’Ĩ", "unable_to_save_date_of_birth": "æ— æŗ•äŋå­˜å‡ēį”Ÿæ—Ĩ期", "unable_to_save_name": "æ— æŗ•æ›´æ–°äēēį‰Šåį§°", @@ -1153,8 +1155,8 @@ "unable_to_trash_asset": "æ— æŗ•į§ģč‡ŗå›žæ”ļįĢ™", "unable_to_unlink_account": "æ— æŗ•č§Ŗé™¤č´Ļåˇå…ŗč”", "unable_to_unlink_motion_video": "æ— æŗ•č§Ŗé™¤åŽžå†ĩ视éĸ‘兺联", - "unable_to_update_album_cover": "æ— æŗ•æ›´æ–°į›¸å†Œå°éĸ", - "unable_to_update_album_info": "æ— æŗ•æ›´æ–°į›¸å†ŒäŋĄæ¯", + "unable_to_update_album_cover": "æ— æŗ•æ›´æ”šį›¸į°ŋ封éĸ", + "unable_to_update_album_info": "æ— æŗ•æ›´æ”šį›¸į°ŋäŋĄæ¯", "unable_to_update_library": "æ— æŗ•æ›´æ–°å›žåē“", "unable_to_update_location": "æ— æŗ•æ›´æ–°äŊįŊŽäŋĄæ¯", "unable_to_update_settings": "æ— æŗ•æ›´æ–°čŽžįŊŽ", @@ -1184,7 +1186,7 @@ "expired": "厞čŋ‡æœŸ", "expires_date": "将äēŽ {date} čŋ‡æœŸ", "explore": "æŽĸį´ĸ", - "explorer": "čĩ„æēįŽĄį†å™¨", + "explorer": "æŽĸį´ĸ", "export": "å¯ŧå‡ē", "export_as_json": "å¯ŧå‡ēä¸ē JSON", "export_database": "å¯ŧå‡ē数捎åē“", @@ -1198,13 +1200,13 @@ "failed": "å¤ąč´Ĩ", "failed_count": "å¤ąč´Ĩ: {count}æŦĄ", "failed_to_authenticate": "čēĢäģŊéĒŒč¯å¤ąč´Ĩ", - "failed_to_load_assets": "čĩ„æēåŠ čŊŊå¤ąč´Ĩ", + "failed_to_load_assets": "ᅧቇ/视éĸ‘加čŊŊå¤ąč´Ĩ", "failed_to_load_folder": "文äģļ多加čŊŊå¤ąč´Ĩ", "favorite": "æ”ļ藏", "favorite_action_prompt": "厞æˇģ加 {count} ä¸Ē到æ”ļč—å¤š", "favorite_or_unfavorite_photo": "æ”ļč—æˆ–å–æļˆæ”ļč—į…§į‰‡", "favorites": "æ”ļč—å¤š", - "favorites_page_no_favorites": "æœĒ扞到æ”ļč—įš„čĩ„æē", + "favorites_page_no_favorites": "æœĒ扞到æ”ļč—įš„į…§į‰‡/视éĸ‘", "feature_photo_updated": "更æĸäēēį‰Šå°éĸį…§į‰‡æˆåŠŸ", "features": "功čƒŊ", "features_in_development": "åŧ€å‘ä¸­įš„åŠŸčƒŊ", @@ -1216,7 +1218,7 @@ "filename": "文äģļ名", "filetype": "文äģļįąģ型", "filter": "᭛选", - "filter_description": "į­›é€‰į›Žæ ‡čĩ„æēįš„æĄäģļ", + "filter_description": "į­›é€‰į›Žæ ‡æ–‡äģļįš„æĄäģļ", "filter_people": "᭛选äēēį‰Š", "filter_places": "į­›é€‰åœ°į‚š", "filter_tags": "į­›é€‰æ ‡į­ž", @@ -1248,7 +1250,7 @@ "gps": "有GPSäŋĄæ¯", "gps_missing": "无GPSäŋĄæ¯", "grant_permission": "授权权限", - "group_albums_by": "į›¸å†Œåˆ†įģ„䞝捎...", + "group_albums_by": "ᛏį°ŋ分įģ„䞝捎...", "group_country": "按å›ŊåŽļ分įģ„", "group_no": "不分įģ„", "group_owner": "æŒ‰æ‰€æœ‰č€…åˆ†įģ„", @@ -1268,17 +1270,17 @@ "height": "é̘åēĻ", "hi_user": "您åĨŊīŧŒ{name}īŧˆ{email}īŧ‰", "hide_all_people": "éšč—æ‰€æœ‰äēēį‰Š", - "hide_gallery": "éšč—į›¸å†Œ", + "hide_gallery": "éšč—į›¸į°ŋ", "hide_named_person": "隐藏äēēį‰Šīŧš{name}", "hide_password": "éšč—å¯†į ", "hide_person": "隐藏äēēį‰Š", "hide_schema": "éšč—æ¨Ąåŧ", "hide_text_recognition": "éšč—æ–‡æœŦ蝆åˆĢį쓿žœ", "hide_unnamed_people": "隐藏æœĒå‘Ŋåįš„äēēį‰Š", - "home_page_add_to_album_conflicts": "厞将 {added} ä¸Ē文äģᅫģåŠ åˆ°į›¸å†Œ \"{album}\" 中。å…ļ中有 {failed} ä¸Ē文äģᅵŦæĨå°ąåœ¨į›¸å†Œé‡Œäē†ã€‚", - "home_page_add_to_album_err_local": "暂æ—ļæ— æŗ•å°†æœŦ地文äģᅫģåŠ åˆ°į›¸å†ŒīŧŒæ­Ŗåœ¨čˇŗčŋ‡", - "home_page_add_to_album_success": "åˇ˛æˆåŠŸå°† {added} ä¸Ē文äģᅫģåŠ åˆ°į›¸å†Œ \"{album}\" 中。", - "home_page_album_err_partner": "暂æ—ļæ— æŗ•å°†\"寚斚\"įš„čĩ„äē§æˇģåŠ åˆ°į›¸å†Œä¸­īŧŒæ­Ŗåœ¨čˇŗčŋ‡", + "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_archive_err_local": "暂æ—ļæ— æŗ•åŊ’æĄŖæœŦ地文äģļīŧŒæ­Ŗåœ¨čˇŗčŋ‡", "home_page_archive_err_partner": "æ— æŗ•åŊ’æĄŖåäŊœč€…įš„æ–‡äģļīŧŒæ­Ŗåœ¨čˇŗčŋ‡", "home_page_building_timeline": "æ­Ŗåœ¨æž„åģēæ—ļ间įēŋ", @@ -1286,7 +1288,7 @@ "home_page_delete_remote_err_local": "æŖ€æĩ‹åˆ°åž…åˆ é™¤åˆ—čĄ¨ä¸­åŒ…åĢä熿œŦ地文äģļīŧŒæ­Ŗåœ¨čˇŗčŋ‡", "home_page_favorite_err_local": "暂不支持æ”ļ藏æœŦ地文äģļīŧŒæ­Ŗåœ¨čˇŗčŋ‡", "home_page_favorite_err_partner": "暂不支持æ”ļč—åäŊœč€…įš„æ–‡äģļīŧŒæ­Ŗåœ¨čˇŗčŋ‡", - "home_page_first_time_notice": "åĻ‚æžœæ‚¨æ˜¯éĻ–æŦĄäŊŋᔍæœŦåē”ᔍīŧŒč¯ˇåŠĄåŋ…选拊一ä¸Ē备äģŊį›¸å†ŒīŧŒäģĨäžŋæ—ļ间įēŋčƒŊäģŽä¸­čŽˇå–åšļåą•į¤ēį…§į‰‡å’Œč§†éĸ‘", + "home_page_first_time_notice": "åĻ‚æžœæ‚¨æ˜¯éĻ–æŦĄäŊŋᔍæœŦåē”ᔍīŧŒč¯ˇåŠĄåŋ…选拊一ä¸Ē备äģŊᛏį°ŋīŧŒäģĨäžŋæ—ļ间įēŋčƒŊäģŽä¸­čŽˇå–åšļåą•į¤ēį…§į‰‡å’Œč§†éĸ‘", "home_page_locked_error_local": "æ— æŗ•å°†æœŦ地文äģļį§ģå…Ĩé”åŽšįš„æ–‡äģļ多īŧŒæ­Ŗåœ¨čˇŗčŋ‡", "home_page_locked_error_partner": "æ— æŗ•å°†åäŊœč€…įš„æ–‡äģļį§ģå…Ĩé”åŽšįš„æ–‡äģļ多īŧŒæ­Ŗåœ¨čˇŗčŋ‡", "home_page_share_err_local": "æ— æŗ•é€ščŋ‡é“žæŽĨ分äēĢæœŦ地文äģļīŧŒæ­Ŗåœ¨čˇŗčŋ‡", @@ -1313,17 +1315,17 @@ "image_viewer_page_state_provider_download_started": "åŧ€å§‹ä¸‹čŊŊ", "image_viewer_page_state_provider_download_success": "下čŊŊ成功", "image_viewer_page_state_provider_share_error": "分äēĢå‡ē错", - "immich_logo": "Immich 标åŋ—", - "immich_web_interface": "Immich įŊ‘éĄĩį•Œéĸ", - "import_from_json": "äģŽ JSON å¯ŧå…Ĩ", + "immich_logo": "Immich Logo", + "immich_web_interface": "ImmichįŊ‘éĄĩį•Œéĸ", + "import_from_json": "äģŽJSONå¯ŧå…Ĩ", "import_path": "å¯ŧå…Ĩčˇ¯åž„", - "in_albums": "在{count, plural, one {# ä¸Ēį›¸å†Œ} other {# ä¸Ēį›¸å†Œ}}中", + "in_albums": "在{count, plural, one {# ä¸Ēᛏį°ŋ} other {# ä¸Ēᛏį°ŋ}}中", "in_archive": "厞åŊ’æĄŖ", "in_year": "{year}åš´", "in_year_selector": "在", "include_archived": "包æ‹Ŧ厞åŊ’æĄŖ", - "include_shared_albums": "包æ‹Ŧå…ąäēĢį›¸å†Œ", - "include_shared_partner_assets": "包æ‹Ŧ协äŊœč€…å…ąäēĢčĩ„æē", + "include_shared_albums": "包æ‹Ŧå…ąäēĢᛏį°ŋ", + "include_shared_partner_assets": "包æ‹Ŧ协äŊœč€…å…ąäēĢᅧቇ/视éĸ‘", "individual_share": "单į‹Ŧ分äēĢ", "individual_shares": "单į‹Ŧ分äēĢ", "info": "äŋĄæ¯", @@ -1336,20 +1338,20 @@ "invalid_date": "æ— æ•ˆįš„æ—Ĩ期", "invalid_date_format": "æ— æ•ˆįš„æ—Ĩ期æ ŧåŧ", "invite_people": "邀蝎äēē员", - "invite_to_album": "é‚€č¯ˇåŠ å…Ĩį›¸å†Œ", + "invite_to_album": "é‚€č¯ˇåŠ å…Ĩᛏį°ŋ", "ios_debug_info_fetch_ran_at": "čŋčĄŒæ‹‰å– {dateTime}", "ios_debug_info_last_sync_at": "上æŦĄåŒæ­ĨäēŽ {dateTime}", "ios_debug_info_no_processes_queued": "æ— åž…å¤„į†įš„åŽå°čŋ›į¨‹", "ios_debug_info_no_sync_yet": "尚æœĒæ‰§čĄŒåŽå°åŒæ­ĨäģģåŠĄ", - "ios_debug_info_processes_queued": "{count, plural, one {{count} ä¸Ē后台äģģåŠĄåœ¨æŽ’é˜Ÿ} other {{count} ä¸Ē后台äģģåŠĄåœ¨æŽ’é˜Ÿ}}", + "ios_debug_info_processes_queued": "{count, plural, one {{count}ä¸Ē后台äģģåŠĄåœ¨æŽ’é˜Ÿ} other {{count}ä¸Ē后台äģģåŠĄåœ¨æŽ’é˜Ÿ}}", "ios_debug_info_processing_ran_at": "处ᐆäēŽ {dateTime}", - "items_count": "{count, plural, one {#ä¸Ē} other {#ä¸Ē}}", + "items_count": "{count, plural, one {#ä¸Ē} other {#ä¸Ē}}éĄšį›Ž", "jobs": "äģģåŠĄ", "json_editor": "JSONįŧ–čž‘å™¨", "json_error": "JSON错蝝", "keep": "äŋį•™", - "keep_albums": "äŋį•™į›¸å†Œ", - "keep_albums_count": "äŋį•™ {count} {count, plural, one {ä¸Ēį›¸å†Œ} other {ä¸Ēį›¸å†Œ}}", + "keep_albums": "äŋį•™į›¸į°ŋ", + "keep_albums_count": "äŋį•™ {count} {count, plural, one {ä¸Ēᛏį°ŋ} other {ä¸Ēᛏį°ŋ}}", "keep_all": "全部äŋį•™", "keep_description": "选拊释攞įŠē间æ—ļäŋį•™åœ¨čŽžå¤‡ä¸Šįš„å†…åŽšã€‚", "keep_favorites": "äŋį•™æ”ļ藏", @@ -1357,7 +1359,7 @@ "keep_on_device_hint": "选拊čρäŋį•™åœ¨æœŦčŽžå¤‡ä¸Šįš„éĄšį›Ž", "keep_this_delete_others": "äŋį•™æ­¤éĄšīŧŒå…ļäŊ™åˆ é™¤", "keeping": "äŋį•™: {items}", - "kept_this_deleted_others": "äŋį•™č¯Ĩčĩ„æēåšļ删除 {count, plural, one {# ä¸Ēčĩ„æē} other {# ä¸Ēčĩ„æē}}", + "kept_this_deleted_others": "äŋį•™č¯Ĩᅧቇ/视éĸ‘åšļ删除{count, plural, one {# ä¸ĒéĄšį›Ž} other {# ä¸ĒéĄšį›Ž}}", "keyboard_shortcuts": "é”Žį›˜åŋĢæˇé”Ž", "language": "蝭荀", "language_no_results_subtitle": "å°č¯•č°ƒæ•´æ‚¨įš„æœį´ĸč¯", @@ -1366,45 +1368,47 @@ "language_setting_description": "é€‰æ‹Šæ‚¨įš„éϖ选蝭荀", "large_files": "大文äģļ", "last": "最后一ä¸Ē", - "last_months": "{count, plural, one {上ä¸Ē月} other {最čŋ‘ # ä¸Ē月}}", + "last_months": "{count, plural, one {上ä¸Ē月} other {最čŋ‘#ä¸Ē月}}", "last_seen": "最后上įēŋäēŽ", "latest_version": "æœ€æ–°į‰ˆæœŦ", "latitude": "įēŦåēĻ", "leave": "įĻģåŧ€", - "leave_album": "įĻģåŧ€į›¸å†Œ", + "leave_album": "įĻģåŧ€į›¸į°ŋ", "lens_model": "é•œå¤´åž‹åˇ", "let_others_respond": "å…čŽ¸äģ–äēē回复", "level": "į­‰įē§", - "library": "čĩ„æ–™åē“", + "library": "čĩ„æēåē“", "library_add_folder": "æˇģ加文äģļ多", "library_edit_folder": "įŧ–čž‘æ–‡äģļ多", - "library_options": "čĩ„æ–™åē“选饚", - "library_page_device_albums": "čŽžå¤‡ä¸Šįš„į›¸å†Œ", - "library_page_new_album": "新åģēį›¸å†Œ", - "library_page_sort_asset_count": "čĩ„æēæ•°é‡", + "library_options": "čĩ„æēåē“选饚", + "library_page_device_albums": "čŽžå¤‡ä¸Šįš„į›¸į°ŋ", + "library_page_new_album": "新åģēᛏį°ŋ", + "library_page_sort_asset_count": "éĄšį›Žæ•°é‡", "library_page_sort_created": "创åģēæ—Ĩ期", "library_page_sort_last_modified": "上æŦĄäŋŽæ”š", - "library_page_sort_title": "į›¸å†Œæ ‡éĸ˜", + "library_page_sort_title": "ᛏį°ŋ标éĸ˜", "licenses": "čŽ¸å¯č¯", "light": "æĩ…色", + "light_theme": "切æĸ到æĩ…色ä¸ģéĸ˜", "like": "į‚ščĩž", "like_deleted": "取æļˆį‚ščĩž", "link_motion_video": "链æŽĨåŠ¨æ€č§†éĸ‘", + "link_to_docs": "更多äŋĄæ¯, č¯ˇå‚č§ æ–‡æĄŖ.", "link_to_oauth": "įģ‘åޚ OAuth", "linked_oauth_account": "厞įģ‘åŽšįš„ OAuth č´Ļæˆˇ", "list": "åˆ—čĄ¨", "loading": "加čŊŊ中", "loading_search_results_failed": "加čŊŊ搜į´ĸį쓿žœå¤ąč´Ĩ", "local": "æœŦ地", - "local_asset_cast_failed": "æ— æŗ•å¤„į†å°šæœĒ上äŧ č‡ŗæœåŠĄå™¨įš„čĩ„äē§", - "local_assets": "æœŦ地čĩ„æē", + "local_asset_cast_failed": "æ— æŗ•å¤„į†å°šæœĒ上äŧ č‡ŗæœåŠĄå™¨įš„į…§į‰‡/视éĸ‘", + "local_assets": "æœŦåœ°éĄšį›Ž", "local_id": "æœŦ地 ID", "local_media_summary": "æœŦ地åĒ’äŊ“摘čρ", "local_network": "æœŦ地įŊ‘įģœ", "local_network_sheet_info": "äŊŋį”¨æŒ‡åŽšįš„ Wi-Fi įŊ‘į윿—ļīŧŒåē”į”¨å°†é€ščŋ‡æ­¤ URL čŋžæŽĨåˆ°æœåŠĄå™¨", "location": "äŊįŊŽ", "location_permission": "厚äŊæƒé™", - "location_permission_content": "ä¸ēäē†äŊŋᔍč‡Ē动切æĸ功čƒŊīŧŒImmich 需čĻčŽˇå–į˛žįĄŽäŊįŊŽæƒé™īŧŒäģĨäžŋč¯ģ取åŊ“前 Wi-Fi įŊ‘įģœįš„åį§°", + "location_permission_content": "ä¸ēäē†äŊŋᔍč‡Ē动切æĸ功čƒŊīŧŒImmich需čĻčŽˇå–į˛žįĄŽäŊįŊŽæƒé™īŧŒäģĨäžŋč¯ģ取åŊ“前 Wi-Fi įŊ‘įģœįš„åį§°", "location_picker_choose_on_map": "äģŽåœ°å›žé€‰å–", "location_picker_latitude_error": "č¯ˇčž“å…Ĩæœ‰æ•ˆįš„įēŦåēĻ", "location_picker_latitude_hint": "č¯ˇåœ¨æ­¤å¤„čž“å…ĨįēŦåēĻ", @@ -1420,7 +1424,7 @@ "logged_out_device": "厞退å‡ēčŽžå¤‡į™ģåŊ•", "login": "į™ģåŊ•", "login_disabled": "į™ģåŊ•功čƒŊ厞įρᔍ", - "login_form_api_exception": "API åŧ‚å¸¸ã€‚č¯ˇæŖ€æŸĨæœåŠĄå™¨ URL åšļé‡č¯•ã€‚", + "login_form_api_exception": "APIåŧ‚å¸¸ã€‚č¯ˇæŖ€æŸĨæœåŠĄå™¨URLåšļé‡č¯•ã€‚", "login_form_back_button_text": "čŋ”回", "login_form_email_hint": "youremail@email.com", "login_form_endpoint_hint": "http://æ‚¨įš„æœåŠĄå™¨åœ°å€:įĢ¯åŖ", @@ -1430,7 +1434,7 @@ "login_form_err_invalid_url": "æ— æ•ˆįš„URL", "login_form_err_leading_whitespace": "ä¸å…čŽ¸å‰å¯ŧįŠēæ ŧ", "login_form_err_trailing_whitespace": "ä¸å…čŽ¸å°žéšįŠēæ ŧ", - "login_form_failed_get_oauth_server_config": "äŊŋᔍ OAuth į™ģåŊ•å‡ē错īŧŒč¯ˇæŖ€æŸĨæœåŠĄå™¨åœ°å€", + "login_form_failed_get_oauth_server_config": "äŊŋᔍOAuthį™ģåŊ•å‡ē错īŧŒč¯ˇæŖ€æŸĨæœåŠĄå™¨åœ°å€", "login_form_failed_get_oauth_server_disable": "æ­¤æœåŠĄå™¨ä¸æ”¯æŒ OAuth 功čƒŊ", "login_form_failed_login": "į™ģåŊ•å¤ąč´ĨīŧŒč¯ˇæŖ€æŸĨæœåŠĄå™¨åœ°å€ã€é‚ŽįŽąå’Œå¯†į ", "login_form_handshake_exception": "ä¸ŽæœåŠĄå™¨įš„æĄæ‰‹åŧ‚常。åĻ‚æžœæ‚¨äŊŋį”¨įš„æ˜¯č‡Ēį­žåč¯äšĻīŧŒč¯ˇåœ¨čŽžįŊŽä¸­åŧ€å¯å¯šč‡Ēį­žåč¯äšĻįš„æ”¯æŒã€‚", @@ -1459,8 +1463,8 @@ "maintenance_restore_library": "æĸå¤æ‚¨įš„å›žåē“", "maintenance_restore_library_confirm": "åĻ‚æžœįĄŽčŽ¤æ— č¯¯īŧŒč¯ˇįģ§įģ­čŋ›čĄŒå¤‡äģŊæĸ复īŧ", "maintenance_restore_library_description": "æ­Ŗåœ¨æĸ复数捎åē“", - "maintenance_restore_library_folder_has_files": "{folder} 包åĢ {count} ä¸Ē文äģļ多", - "maintenance_restore_library_folder_no_files": "{folder} įŧē少文äģļīŧ", + "maintenance_restore_library_folder_has_files": "{folder}包åĢ{count}ä¸Ē文äģļ多", + "maintenance_restore_library_folder_no_files": "{folder}įŧē少文äģļīŧ", "maintenance_restore_library_folder_pass": "可č¯ģ可写", "maintenance_restore_library_folder_read_fail": "不可č¯ģ", "maintenance_restore_library_folder_write_fail": "不可写", @@ -1495,14 +1499,14 @@ "map_location_service_disabled_title": "厚äŊæœåŠĄåˇ˛įρᔍ", "map_marker_for_images": "æ ‡čŽ°{city}、{country}æ‹æ‘„į…§į‰‡įš„åœ°å›žå›žæ ‡", "map_marker_with_image": "å¸Ļéĸ„č§ˆå›žįš„åœ°å›žæ ‡čް", - "map_no_location_permission_content": "需čρäŊįŊŽæƒé™æ‰čƒŊ昞į¤ē您åŊ“前äŊįŊŽįš„čĩ„æēã€‚įŽ°åœ¨čĻå…čŽ¸å—īŧŸ", + "map_no_location_permission_content": "需čρäŊįŊŽæƒé™æ‰čƒŊ昞į¤ē您åŊ“前äŊįŊŽįš„ᅧቇ/视éĸ‘ã€‚įŽ°åœ¨čĻå…čŽ¸å—īŧŸ", "map_no_location_permission_title": "äŊįŊŽæƒé™čĸĢæ‹’įģ", "map_settings": "åœ°å›žčŽžįŊŽ", "map_settings_dark_mode": "æˇąč‰˛æ¨Ąåŧ", "map_settings_date_range_option_day": "čŋ‡åŽģ24小æ—ļ", - "map_settings_date_range_option_days": "{days} 夊前", + "map_settings_date_range_option_days": "{days}夊前", "map_settings_date_range_option_year": "1嚴前", - "map_settings_date_range_option_years": "{years} 嚴前", + "map_settings_date_range_option_years": "{years}嚴前", "map_settings_dialog_title": "åœ°å›žčŽžįŊŽ", "map_settings_include_show_archived": "包åĢ厞åŊ’æĄŖįš„å†…åŽš", "map_settings_include_show_partners": "包åĢ协äŊœč€…", @@ -1513,7 +1517,7 @@ "mark_as_read": "æ ‡čŽ°ä¸ē厞č¯ģ", "marked_all_as_read": "åˇ˛å…¨éƒ¨æ ‡čŽ°ä¸ē厞č¯ģ", "matches": "åŒšé…éĄš", - "matching_assets": "åŒšé…įš„čĩ„æē", + "matching_assets": "åŒšé…įš„éĄšį›Ž", "media_type": "åĒ’äŊ“įąģ型", "memories": "é‚Ŗåš´ä슿—Ĩ", "memories_all_caught_up": "åˇ˛å¤„į†åŽŒæ¯•", @@ -1549,15 +1553,15 @@ "move_to_device_trash": "į§ģč‡ŗčŽžå¤‡å›žæ”ļįĢ™", "move_to_lock_folder_action_prompt": "厞将 {count} 饚æˇģ加到锁厚文äģļ多", "move_to_locked_folder": "į§ģč‡ŗé”åŽšæ–‡äģļ多", - "move_to_locked_folder_confirmation": "čŋ™äē›į…§į‰‡å’Œč§†éĸ‘å°†äģŽæ‰€æœ‰į›¸å†Œä¸­į§ģ除īŧŒäģ…可在锁厚文äģļ多内æŸĨįœ‹", + "move_to_locked_folder_confirmation": "čŋ™äē›į…§į‰‡å’Œč§†éĸ‘å°†äģŽæ‰€æœ‰į›¸į°ŋ中į§ģ除īŧŒäģ…可在锁厚文äģļ多内æŸĨįœ‹", "move_up": "向上į§ģ动", - "moved_to_archive": "厞将 {count, plural, one {# 饚čĩ„äē§} other {# 饚čĩ„äē§}} į§ģ臺åŊ’æĄŖ", - "moved_to_library": "厞将 {count, plural, one {# 饚čĩ„äē§} other {# 饚čĩ„äē§}} į§ģ臺čĩ„æ–™åē“", + "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": "æ— æŗ•įŧ–čž‘åĒč¯ģčĩ„æēįš„äŊįŊŽäŋĄæ¯īŧŒæ­Ŗåœ¨čˇŗčŋ‡", + "multiselect_grid_edit_date_time_err_read_only": "æ— æŗ•įŧ–čž‘åĒč¯ģéĄšį›Žįš„æ—Ĩ期īŧŒæ­Ŗåœ¨čˇŗčŋ‡", + "multiselect_grid_edit_gps_err_read_only": "æ— æŗ•įŧ–čž‘åĒč¯ģéĄšį›Žįš„äŊįŊŽäŋĄæ¯īŧŒæ­Ŗåœ¨čˇŗčŋ‡", "mute_memories": "é™éŸŗå›žåŋ†", - "my_albums": "æˆ‘įš„į›¸å†Œ", + "my_albums": "æˆ‘įš„į›¸į°ŋ", "name": "åį§°", "name_or_nickname": "åį§°æˆ–æ˜ĩį§°", "name_required": "åį§°æ˜¯åŋ…åĄĢ饚", @@ -1570,7 +1574,7 @@ "networking_settings": "įŊ‘įģœčŽžįŊŽ", "networking_subtitle": "įŽĄį†æœåŠĄå™¨įĢ¯į‚ščŽžįŊŽ", "never": "永不čŋ‡æœŸ", - "new_album": "新åģēį›¸å†Œ", + "new_album": "新åģēᛏį°ŋ", "new_api_key": "新åĸž API 密é’Ĩ", "new_date_range": "æ–°įš„æ—ĨæœŸčŒƒå›´", "new_password": "æ–°å¯†į ", @@ -1586,16 +1590,16 @@ "next_memory": "下一ä¸Ē回åŋ†", "no": "åĻ", "no_actions_added": "尚æœĒæˇģ加动äŊœ", - "no_albums_found": "æœĒæ‰žåˆ°į›¸å†Œ", - "no_albums_message": "创åģēį›¸å†ŒäģĨæ•´į†æ‚¨įš„į…§į‰‡å’Œč§†éĸ‘", - "no_albums_with_name_yet": "įœ‹čĩˇæĨčŋ˜æ˛Ąæœ‰åŒåįš„į›¸å†Œã€‚", - "no_albums_yet": "įœ‹čĩˇæĨ您čŋ˜æ˛Ąæœ‰åˆ›åģēäģģäŊ•į›¸å†Œã€‚", + "no_albums_found": "æœĒæ‰žåˆ°į›¸į°ŋ", + "no_albums_message": "创åģēᛏį°ŋäģĨæ•´į†æ‚¨įš„į…§į‰‡å’Œč§†éĸ‘", + "no_albums_with_name_yet": "æ˛Ąæœ‰åŒåįš„į›¸į°ŋ。", + "no_albums_yet": "您čŋ˜æ˛Ąæœ‰åˆ›åģēäģģäŊ•ᛏį°ŋ。", "no_archived_assets_message": "åŊ’æĄŖį…§į‰‡å’Œč§†éĸ‘īŧŒå°†å…ļäģŽâ€œį…§į‰‡â€č§†å›žä¸­éšč—", "no_assets_message": "į‚šå‡ģ上äŧ äŊ įš„įŦŦ一åŧ į…§į‰‡", "no_assets_to_show": "暂无内厚可昞į¤ē", "no_cast_devices_found": "æœĒæ‰žåˆ°å¯į”¨įš„æŠ•åąčŽžå¤‡", - "no_checksum_local": "æ— å¯į”¨įš„æ ĄéĒŒå’Œ — æ— æŗ•čŽˇå–æœŦ地čĩ„æē", - "no_checksum_remote": "æ— å¯į”¨įš„æ ĄéĒŒå’Œ — æ— æŗ•čŽˇå–čŋœį¨‹čĩ„æē", + "no_checksum_local": "æ— å¯į”¨įš„æ ĄéĒŒå’Œ — æ— æŗ•čŽˇå–æœŦåœ°éĄšį›Ž", + "no_checksum_remote": "æ— å¯į”¨įš„æ ĄéĒŒå’Œ — æ— æŗ•čŽˇå–čŋœį¨‹éĄšį›Ž", "no_configuration_needed": "无需配įŊŽ", "no_devices": "æš‚æ— åˇ˛æŽˆæƒįš„čŽžå¤‡", "no_duplicates_found": "æœĒå‘įŽ°é‡å¤å†…åŽšã€‚", @@ -1604,22 +1608,22 @@ "no_favorites_message": "æˇģ加æ”ļ藏īŧŒäģĨäžŋåŋĢ速扞到äŊ æœ€į˛žåŊŠįš„į…§į‰‡å’Œč§†éĸ‘", "no_filters_added": "尚æœĒæˇģåŠ į­›é€‰æĄäģļ", "no_libraries_message": "创åģē外部回åē“īŧŒäģĨæĩč§ˆäŊ įš„į…§į‰‡å’Œč§†éĸ‘", - "no_local_assets_found": "æœĒæ‰žåˆ°å…ˇæœ‰æ­¤æ ĄéĒŒå’Œįš„æœŦ地čĩ„æē", + "no_local_assets_found": "æœĒæ‰žåˆ°å…ˇæœ‰æ­¤æ ĄéĒŒå’Œįš„æœŦåœ°éĄšį›Ž", "no_location_set": "æœĒ莞įŊŽäŊįŊŽ", "no_locked_photos_message": "锁厚文äģļå¤šä¸­įš„į…§į‰‡å’Œč§†éĸ‘äŧščĸĢ隐藏īŧŒåœ¨æĩč§ˆæˆ–搜į´ĸ回å瓿—ļ不äŧšæ˜žį¤ē。", "no_name": "æœĒå‘Ŋ名äēēį‰Š", "no_notifications": "暂无通įŸĨ", "no_people_found": "æœĒæ‰žåˆ°åŒšé…įš„äēēį‰Š", "no_places": "æš‚æ— åœ°į‚š", - "no_remote_assets_found": "æœĒæ‰žåˆ°å…ˇæœ‰æ­¤æ ĄéĒŒå’Œįš„čŋœį¨‹čĩ„æē", + "no_remote_assets_found": "æœĒæ‰žåˆ°å…ˇæœ‰æ­¤æ ĄéĒŒå’Œįš„čŋœį¨‹éĄšį›Ž", "no_results": "æœĒ扞到äģģäŊ•åŒšé…éĄš", "no_results_description": "č¯ˇå°č¯•äŊŋį”¨åŒäš‰č¯æˆ–æ›´åŽŊæŗ›įš„å…ŗé”Žč¯", - "no_shared_albums_message": "创åģēį›¸å†ŒīŧŒä¸Žį¤žäē¤åœˆå†…įš„åĨŊå‹å…ąäēĢį…§į‰‡å’Œč§†éĸ‘", + "no_shared_albums_message": "创åģēᛏį°ŋīŧŒä¸Žį¤žäē¤åœˆå†…įš„åĨŊå‹å…ąäēĢį…§į‰‡å’Œč§†éĸ‘", "no_uploads_in_progress": "暂无上äŧ äģģåŠĄ", "none": "无", "not_allowed": "ä¸å…čŽ¸", "not_available": "ä¸é€‚į”¨", - "not_in_any_album": "æœĒæ”ļåŊ•äēŽäģģäŊ•į›¸å†Œ", + "not_in_any_album": "æœĒæ”ļåŊ•äēŽäģģäŊ•ᛏį°ŋ", "not_selected": "æœĒ选拊", "notes": "å¤‡æŗ¨", "nothing_here_yet": "暂无内厚", @@ -1631,10 +1635,10 @@ "notifications": "通įŸĨ", "notifications_setting_description": "įŽĄį†é€šįŸĨ", "oauth": "OAuth", - "obtainium_configurator": "Obtainium配įŊŽå™¨", + "obtainium_configurator": "Obtainium 配įŊŽå™¨", "obtainium_configurator_instructions": "äŊŋᔍ Obtainium į›´æŽĨäģŽ Immich įš„ GitHub 发布éĄĩåŽ‰čŖ…å’Œæ›´æ–° Android åē”į”¨ã€‚č¯ˇåˆ›åģē一ä¸Ē API 密é’Ĩåšļ选拊一ä¸Ēį‰ˆæœŦīŧŒäģĨį”ŸæˆäŊ įš„ Obtainium 配įŊŽé“žæŽĨ", "ocr": "OCR", - "official_immich_resources": "Immich 厘斚čĩ„æē", + "official_immich_resources": "Immich厘斚čĩ„æē", "offline": "įĻģįēŋ", "offset": "偏į§ģ量", "ok": "įĄŽåŽš", @@ -1657,14 +1661,14 @@ "open_the_search_filters": "打åŧ€æœį´ĸ᭛选", "options": "选项", "or": "或", - "organize_into_albums": "æ•´į†åˆ°į›¸å†Œä¸­", - "organize_into_albums_description": "äŊŋᔍåŊ“å‰įš„åŒæ­Ĩ莞įŊŽīŧŒå°†įŽ°æœ‰į…§į‰‡åŊ’å…Ĩį›¸å†Œ", - "organize_your_library": "æ•´į†æ‚¨įš„čĩ„æ–™åē“", + "organize_into_albums": "æ•´į†åˆ°į›¸į°ŋ中", + "organize_into_albums_description": "äŊŋᔍåŊ“å‰įš„åŒæ­Ĩ莞įŊŽīŧŒå°†įŽ°æœ‰į…§į‰‡åŊ’å…Ĩᛏį°ŋ", + "organize_your_library": "æ•´į†æ‚¨įš„čĩ„æēåē“", "original": "åŽŸå§‹įš„", - "other": "å…ļ厃", - "other_devices": "å…ļåŽƒčŽžå¤‡", + "other": "å…ļäģ–", + "other_devices": "å…ļäģ–čŽžå¤‡", "other_entities": "å…ļäģ–厞äŊ“", - "other_variables": "å…ļ厃变量", + "other_variables": "å…ļäģ–变量", "owned": "æˆ‘įš„", "owner": "æ‰€æœ‰č€…", "page": "éĄĩéĸ", @@ -1679,7 +1683,7 @@ "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": "坆᠁", @@ -1700,15 +1704,15 @@ "people": "äēēį‰Š", "people_edits_count": "{count, plural, one {#ä¸Ēäēēį‰Š} other {#ä¸Ēäēēį‰Š}}厞įŧ–čž‘", "people_feature_description": "按äēēį‰Šåˆ†į섿ĩč§ˆį…§į‰‡å’Œč§†éĸ‘", - "people_selected": "{count, plural, one {厞选䏭 # äēē} other {厞选䏭 # äēē}}", + "people_selected": "{count, plural, one {厞选䏭#äēē} other {厞选䏭#äēē}}", "people_sidebar_description": "åœ¨äž§čžšæ æ˜žį¤ē“äēēį‰Šâ€é“žæŽĨ", "permanent_deletion_warning": "永䚅删除č­Ļ告", - "permanent_deletion_warning_setting_description": "永䚅删除čĩ„æēæ—ļ昞į¤ēč­Ļ告", + "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 {#ä¸Ēčĩ„æē}}", + "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": "čŋ”回", @@ -1718,19 +1722,19 @@ "permission_onboarding_permission_denied": "权限čĸĢæ‹’įģã€‚čρäŊŋᔍ ImmichīŧŒč¯ˇåœ¨â€œčŽžįŊŽâ€ä¸­æŽˆäēˆį…§į‰‡å’Œč§†éĸ‘权限。", "permission_onboarding_permission_granted": "æƒé™åˇ˛æŽˆäēˆīŧä¸€åˆ‡å‡†å¤‡å°ąįģĒ。", "permission_onboarding_permission_limited": "权限受限。čĻčŽŠ Immich 备äģŊåšļįŽĄį†äŊ įš„æ•´ä¸Ē回åē“īŧŒč¯ˇåœ¨â€œčŽžįŊŽâ€ä¸­æŽˆäēˆį…§į‰‡å’Œč§†éĸ‘权限。", - "permission_onboarding_request": "Immich 需čĻæƒé™æ‰čƒŊæŸĨįœ‹äŊ įš„į…§į‰‡å’Œč§†éĸ‘。", + "permission_onboarding_request": "Immich需čĻæƒé™æ‰čƒŊæŸĨįœ‹äŊ įš„į…§į‰‡å’Œč§†éĸ‘。", "person": "äēēį‰Š", - "person_age_months": "{months, plural, one {# ä¸Ē月} other {# ä¸Ē月}}大", + "person_age_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 {}}", + "person_hidden": "{name}{hidden, select, true {īŧˆéšč—īŧ‰} other {}}", "person_recognized": "厞蝆åˆĢäēēį‰Š", "person_selected": "åˇ˛é€‰æ‹Šäēēį‰Š", "photo_shared_all_users": "įœ‹čĩˇæĨäŊ åˇ˛įģä¸Žæ‰€æœ‰į”¨æˆˇå…ąäēĢäē†äŊ įš„ᅧቇīŧŒæˆ–者äŊ æ˛Ąæœ‰äģģäŊ•可äģĨå…ąäēĢįš„į”¨æˆˇã€‚", "photos": "ᅧቇ", "photos_and_videos": "ᅧቇ & 视éĸ‘", - "photos_count": "{count, plural, one {{count, number} åŧ į…§į‰‡} other {{count, number} åŧ į…§į‰‡}}", + "photos_count": "{count, plural, one {{count, number}åŧ į…§į‰‡} other {{count, number}åŧ į…§į‰‡}}", "photos_from_previous_years": "é‚Ŗåš´ä슿—Ĩ", "photos_only": "äģ…ᅧቇ", "pick_a_location": "选拊äŊįŊŽ", @@ -1742,13 +1746,13 @@ "pin_verification": "PIN᠁énj蝁", "place": "åœ°į‚š", "places": "åœ°į‚š", - "places_count": "{count, plural, one {{count, number} ä¸Ēåœ°į‚š} other {{count, number} ä¸Ēåœ°į‚š}}", + "places_count": "{count, plural, one {{count, number}ä¸Ēåœ°į‚š} other {{count, number}ä¸Ēåœ°į‚š}}", "play": "播攞", "play_memories": "æ’­æ”žé‚Ŗåš´ä슿—Ĩ", "play_motion_photo": "æ’­æ”žåŠ¨æ€å›žį‰‡", "play_or_pause_video": "æ’­æ”žæˆ–æš‚åœč§†éĸ‘", "play_original_video": "æ’­æ”žåŽŸå§‹č§†éĸ‘", - "play_original_video_setting_description": "äŧ˜å…ˆæ’­æ”žåŽŸå§‹č§†éĸ‘īŧŒč€ŒéžčŊŦ᠁视éĸ‘。åĻ‚æžœåŽŸå§‹čĩ„æēä¸å…ŧ厚īŧŒå¯čƒŊæ— æŗ•æ­Ŗå¸¸æ’­æ”žã€‚", + "play_original_video_setting_description": "äŧ˜å…ˆæ’­æ”žåŽŸå§‹č§†éĸ‘īŧŒč€ŒéžčŊŦ᠁视éĸ‘。åĻ‚æžœåŽŸå§‹č§†éĸ‘不å…ŧ厚īŧŒå¯čƒŊæ— æŗ•æ­Ŗå¸¸æ’­æ”žã€‚", "play_transcoded_video": "播攞čŊŦ᠁视éĸ‘", "please_auth_to_access": "蝎čŋ›čĄŒčēĢäģŊénj蝁äģĨčŽŋ问", "port": "įĢ¯åŖ", @@ -1772,7 +1776,7 @@ "profile_drawer_readonly_mode": "åĒč¯ģæ¨Ąåŧåˇ˛å¯į”¨ã€‚é•ŋæŒ‰į”¨æˆˇå¤´åƒå›žæ ‡é€€å‡ē。", "profile_image_of_user": "{user}įš„ä¸Ēäēēčĩ„æ–™å›žį‰‡", "profile_picture_set": "ä¸Ēäēēčĩ„æ–™å›žį‰‡åˇ˛čŽžįŊŽã€‚", - "public_album": "å…Ŧåŧ€į›¸å†Œ", + "public_album": "å…Ŧåŧ€į›¸į°ŋ", "public_share": "å…Ŧåŧ€å…ąäēĢ", "purchase_account_info": "æ”¯æŒč€…", "purchase_activated_subtitle": "感č°ĸ您寚 Immich 和åŧ€æēčŊ¯äģļįš„æ”¯æŒ", @@ -1806,9 +1810,9 @@ "purchase_server_description_2": "æ”¯æŒč€…įŠļ态", "purchase_server_title": "æœåŠĄå™¨", "purchase_settings_server_activated": "æœåŠĄå™¨äē§å“å¯†é’Ĩæ­Ŗåœ¨į”ąįŽĄį†å‘˜įŽĄį†", - "query_asset_id": "æŸĨč¯ĸčĩ„äē§ID", + "query_asset_id": "æŸĨč¯ĸéĄšį›ŽID", "queue_status": "排队中 {count}/{total}", - "rate_asset": "čĩ„äē§æ˜Ÿįē§", + "rate_asset": "éĄšį›Žæ˜Ÿįē§", "rating": "星įē§", "rating_clear": "删除星įē§", "rating_count": "{count, plural, =0 {æœĒ蝄įē§} one {# 星} other {# 星}}", @@ -1823,7 +1827,7 @@ "reassigned_assets_to_new_person": "重新指洞{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}åˆ°æ–°įš„äēēį‰Š", "reassing_hint": "æŒ‡æ´žé€‰æ‹Šįš„éĄšį›Žåˆ°åˇ˛å­˜åœ¨įš„äēēį‰Š", "recent": "最čŋ‘", - "recent_albums": "最čŋ‘įš„į›¸å†Œ", + "recent_albums": "最čŋ‘įš„į›¸į°ŋ", "recent_searches": "最čŋ‘搜į´ĸ", "recently_added": "čŋ‘期æˇģ加", "recently_added_page_title": "最čŋ‘æˇģ加", @@ -1849,8 +1853,8 @@ "remove_assets_title": "į§ģé™¤éĄšį›ŽīŧŸ", "remove_custom_date_range": "取æļˆč‡Ē厚䚉æ—ĨæœŸčŒƒå›´", "remove_deleted_assets": "åŊģåē•删除文äģļ", - "remove_from_album": "äģŽį›¸å†Œä¸­į§ģ除", - "remove_from_album_action_prompt": "äģŽį›¸å†Œä¸­į§ģ除äē† {count} 饚", + "remove_from_album": "äģŽį›¸į°ŋ中į§ģ除", + "remove_from_album_action_prompt": "äģŽį›¸į°ŋ中į§ģ除äē† {count} 饚", "remove_from_favorites": "į§ģå‡ēæ”ļ藏", "remove_from_lock_folder_action_prompt": "厞äģŽé”åŽšįš„æ–‡äģļ多中į§ģ除 {count} 饚", "remove_from_locked_folder": "äģŽé”åŽšæ–‡äģļ多中į§ģ除", @@ -1867,7 +1871,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": "æœĒ莟č¸Ē和įŧēå¤ąįš„æ–‡äģļ将在此处昞į¤ē", @@ -1895,7 +1899,7 @@ "resolved_all_duplicates": "å¤„į†æ‰€æœ‰é‡å¤éĄš", "restore": "æĸ复", "restore_all": "æĸ复全部", - "restore_trash_action_prompt": "äģŽå›žæ”ļį̙䏭æĸ复äē† {count} 饚", + "restore_trash_action_prompt": "äģŽå›žæ”ļį̙䏭æĸ复äē†{count}饚", "restore_user": "æĸå¤į”¨æˆˇ", "restored_asset": "厞æĸå¤éĄšį›Ž", "resume": "įģ§įģ­", @@ -1921,9 +1925,9 @@ "scan_library": "æ‰Ģ描", "scan_settings": "æ‰ĢæčŽžįŊŽ", "scanning": "æ‰Ģ描中", - "scanning_for_album": "æ‰Ģæį›¸å†Œä¸­...", + "scanning_for_album": "æ‰Ģæį›¸į°ŋ中...", "search": "搜į´ĸ", - "search_albums": "搜į´ĸį›¸å†Œ", + "search_albums": "搜į´ĸᛏį°ŋ", "search_by_context": "通čŋ‡æčŋ°įš„åœē景æŸĨ扞", "search_by_description": "通čŋ‡æčŋ°æŸĨ扞", "search_by_description_example": "åœ¨æ˛™åˇ´åž’æ­Ĩįš„æ—Ĩ子", @@ -1941,7 +1945,7 @@ "search_filter_date": "æ—Ĩ期", "search_filter_date_interval": "äģŽ{start}到{end}", "search_filter_date_title": "选拊æ—ĨæœŸčŒƒå›´", - "search_filter_display_option_not_in_album": "ä¸åœ¨į›¸å†Œä¸­", + "search_filter_display_option_not_in_album": "ä¸åœ¨į›¸į°ŋ中", "search_filter_display_options": "昞į¤ē选项", "search_filter_filename": "通čŋ‡æ–‡äģļ名搜į´ĸ", "search_filter_location": "äŊįŊŽ", @@ -1986,14 +1990,14 @@ "second": "į§’", "see_all_people": "æŸĨįœ‹æ‰€æœ‰äēēį‰Š", "select": "选拊", - "select_album": "é€‰æ‹Šį›¸å†Œ", - "select_album_cover": "é€‰æ‹Šį›¸å†Œå°éĸ", - "select_albums": "é€‰æ‹Šį›¸å†Œ", + "select_album": "é€‰æ‹Šį›¸į°ŋ", + "select_album_cover": "é€‰æ‹Šį›¸į°ŋ封éĸ", + "select_albums": "é€‰æ‹Šį›¸į°ŋ", "select_all": "全选", "select_all_duplicates": "é€‰æ‹Šæ‰€æœ‰é‡å¤éĄš", "select_all_in": "选拊 {group} ä¸­įš„æ‰€æœ‰å†…åŽš", "select_avatar_color": "选拊头像éĸœč‰˛", - "select_count": "{count, plural, one {选拊 # 饚} other {选拊 # 饚}}", + "select_count": "{count, plural, one {厞选䏭#饚} other {厞选䏭#饚}}", "select_cutoff_date": "选拊æˆĒæ­ĸæ—Ĩ期", "select_face": "选拊äēē脸", "select_featured_photo": "选拊ä¸Ē性头像", @@ -2006,14 +2010,14 @@ "select_person_to_tag": "选拊čĻæ ‡čŽ°įš„äēēį‰Š", "select_photos": "é€‰æ‹Šį…§į‰‡", "select_trash_all": "全部删除", - "select_user_for_sharing_page_err_album": "创åģēį›¸å†Œå¤ąč´Ĩ", + "select_user_for_sharing_page_err_album": "创åģēᛏį°ŋå¤ąč´Ĩ", "selected": "åˇ˛é€‰æ‹Š", - "selected_count": "{count, plural, other {#éĄšåˇ˛é€‰æ‹Š}}", + "selected_count": "{count, plural, other {厞选䏭#饚}}", "selected_gps_coordinates": "åˇ˛é€‰åŽšįš„GPS坐标", "send_message": "发送æļˆæ¯", "send_welcome_email": "发送æŦĸčŋŽé‚Žäģļ", "server_endpoint": "æœåŠĄå™¨ URL", - "server_info_box_app_version": "App į‰ˆæœŦ", + "server_info_box_app_version": "Appį‰ˆæœŦ", "server_info_box_server_url": "æœåŠĄå™¨åœ°å€", "server_offline": "æœåŠĄå™¨įĻģįēŋ", "server_online": "æœåŠĄå™¨åœ¨įēŋ", @@ -2024,7 +2028,7 @@ "server_update_available": "æœåŠĄå™¨æ›´æ–°å¯į”¨", "server_version": "æœåŠĄå™¨į‰ˆæœŦ", "set": "莞įŊŽ", - "set_as_album_cover": "莞ä¸ēį›¸å†Œå°éĸ", + "set_as_album_cover": "莞ä¸ēᛏį°ŋ封éĸ", "set_as_featured_photo": "莞įŊŽä¸ēį‰šč‰˛į…§į‰‡", "set_as_profile_picture": "莞ä¸ēä¸Ēäēēčĩ„æ–™å›žį‰‡", "set_date_of_birth": "莞įŊŽå‡ēį”Ÿæ—Ĩ期", @@ -2043,11 +2047,11 @@ "setting_languages_apply": "åē”ᔍ", "setting_languages_subtitle": "更攚åē”ᔍ蝭荀", "setting_notifications_notify_failures_grace_period": "后台备äģŊå¤ąč´Ĩ通įŸĨīŧš{duration}", - "setting_notifications_notify_hours": "{count} 小æ—ļ", + "setting_notifications_notify_hours": "{count}小æ—ļ", "setting_notifications_notify_immediately": "įĢ‹åŗ", - "setting_notifications_notify_minutes": "{count} 分钟", + "setting_notifications_notify_minutes": "{count}分钟", "setting_notifications_notify_never": "äģŽä¸", - "setting_notifications_notify_seconds": "{count} į§’", + "setting_notifications_notify_seconds": "{count}į§’", "setting_notifications_single_progress_subtitle": "æ¯éĄšįš„č¯Ļįģ†ä¸Šäŧ čŋ›åēĻäŋĄæ¯", "setting_notifications_single_progress_title": "昞į¤ē后台备äģŊč¯Ļįģ†čŋ›åēĻ", "setting_notifications_subtitle": "č°ƒæ•´é€šįŸĨéĻ–é€‰éĄš", @@ -2065,22 +2069,22 @@ "share": "分äēĢ", "share_action_prompt": "åˇ˛å…ąäēĢ {count} éĄšį›Ž", "share_add_photos": "æˇģåŠ éĄšį›Ž", - "share_assets_selected": "{count} åˇ˛é€‰æ‹Š", + "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_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_intent_upload_button_progress_text": "厞䏊äŧ {current} / {total}", "shared_link_app_bar_title": "å…ąäēĢ链æŽĨ", "shared_link_clipboard_copied_massage": "复åˆļ到å‰Ēč´´æŋ", "shared_link_clipboard_text": "链æŽĨīŧš{link}\n坆᠁īŧš{password}", @@ -2088,25 +2092,25 @@ "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_days": "{count}夊", "shared_link_edit_expire_after_option_hour": "1小æ—ļ", - "shared_link_edit_expire_after_option_hours": "{count} 小æ—ļ", + "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_expire_after_option_minutes": "{count}分钟", + "shared_link_edit_expire_after_option_months": "{count}ä¸Ē月", + "shared_link_edit_expire_after_option_year": "{count}åš´", "shared_link_edit_password_hint": "输å…Ĩå…ąäēĢ坆᠁", "shared_link_edit_submit_button": "更新铞æŽĨ", "shared_link_error_server_url_fetch": "æ— æŗ•čŽˇå–æœåŠĄå™¨åœ°å€", - "shared_link_expires_day": "{count} 夊后čŋ‡æœŸ", - "shared_link_expires_days": "{count} 夊后čŋ‡æœŸ", - "shared_link_expires_hour": "{count} 小æ—ļ后čŋ‡æœŸ", - "shared_link_expires_hours": "{count} 小æ—ļ后čŋ‡æœŸ", - "shared_link_expires_minute": "{count} 分钟后čŋ‡æœŸ", - "shared_link_expires_minutes": "{count} 分钟后čŋ‡æœŸ", + "shared_link_expires_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_expires_second": "{count}į§’åŽčŋ‡æœŸ", + "shared_link_expires_seconds": "{count}į§’åŽčŋ‡æœŸ", "shared_link_individual_shared": "ä¸Ēäēēå…ąäēĢ", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "įŽĄį†å…ąäēĢ链æŽĨ", @@ -2114,20 +2118,20 @@ "shared_link_password_description": "需čĻå¯†į æ‰čƒŊčŽŋé—Žæ­¤å…ąäēĢ链æŽĨ", "shared_links": "å…ąäēĢ链æŽĨ", "shared_links_description": "通čŋ‡é“žæŽĨ分äēĢį…§į‰‡å’Œč§†éĸ‘", - "shared_photos_and_videos_count": "{assetCount, plural, other {#éĄšåˇ˛å…ąäēĢᅧቇ&视éĸ‘。}}", + "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": "å…ąäēĢį›¸å†Œ", - "sharing_page_description": "创åģēå…ąäēĢį›¸å†ŒäģĨ与įŊ‘įģœä¸­įš„äēēå…ąäēĢį…§į‰‡å’Œč§†éĸ‘。", + "sharing_page_album": "å…ąäēĢᛏį°ŋ", + "sharing_page_description": "创åģēå…ąäēĢᛏį°ŋäģĨ与įŊ‘įģœä¸­įš„äēēå…ąäēĢį…§į‰‡å’Œč§†éĸ‘。", "sharing_page_empty_list": "įŠē", "sharing_sidebar_description": "åœ¨äž§čžšæ ä¸­æ˜žį¤ēâ€œå…ąäēĢ”链æŽĨ", - "sharing_silver_appbar_create_shared_album": "创åģēå…ąäēĢį›¸å†Œ", + "sharing_silver_appbar_create_shared_album": "创åģēå…ąäēĢᛏį°ŋ", "sharing_silver_appbar_share_partner": "å…ąäēĢįģ™åäŊœč€…", "shift_to_permanent_delete": "按äŊ ⇧ Shift é”Žæ°¸äš…åˆ é™¤éĄšį›Ž", - "show_album_options": "昞į¤ēį›¸å†Œé€‰éĄš", - "show_albums": "昞į¤ēį›¸å†Œ", + "show_album_options": "昞į¤ēᛏį°ŋ选项", + "show_albums": "昞į¤ēᛏį°ŋ", "show_all_people": "昞į¤ē所有äēēį‰Š", "show_and_hide_people": "昞į¤ēå’Œéšč—äēēį‰Š", "show_file_location": "昞į¤ē文äģļäŊįŊŽ", @@ -2162,7 +2166,7 @@ "slideshow_repeat": "重复åšģၝቇ", "slideshow_repeat_description": "åšģၝቇį쓿ŸåŽåžĒįŽ¯æ’­æ”ž", "slideshow_settings": "æ”žæ˜ čŽžįŊŽ", - "sort_albums_by": "į›¸å†ŒæŽ’åēäžæŽ...", + "sort_albums_by": "ᛏį°ŋ排åēäžæŽ...", "sort_created": "创åģēæ—Ĩ期", "sort_items": "éĄšį›Žæ•°é‡", "sort_modified": "äŋŽæ”šæ—Ĩ期", @@ -2171,9 +2175,9 @@ "sort_people_by_similarity": "æŒ‰į›¸äŧŧ性寚äēēį‰Ščŋ›čĄŒæŽ’åē", "sort_recent": "æœ€æ–°įš„į…§į‰‡", "sort_title": "标éĸ˜", - "source": "GitHub æēäģŖį ", + "source": "GitHubæēäģŖį ", "stack": "堆叠", - "stack_action_prompt": "{count} ä¸Ēåˇ˛å †å ", + "stack_action_prompt": "{count}ä¸Ēåˇ˛å †å ", "stack_duplicates": "å †å é‡å¤éĄšį›Ž", "stack_select_one_photo": "ä¸ē堆叠选拊一åŧ åą•į¤ē回", "stack_selected_photos": "å †å é€‰åŽšįš„į…§į‰‡", @@ -2187,7 +2191,7 @@ "stop_casting": "停æ­ĸ投攞", "stop_motion_photo": "厚æ ŧᅧቇ", "stop_photo_sharing": "停æ­ĸå…ąäēĢᅧቇīŧŸ", - "stop_photo_sharing_description": "“{partner}”将不čƒŊčŽŋé—Žæ‚¨įš„į…§į‰‡ã€‚", + "stop_photo_sharing_description": "{partner}将不čƒŊčŽŋé—Žæ‚¨įš„į…§į‰‡ã€‚", "stop_sharing_photos_with_user": "停æ­ĸä¸Žæ­¤į”¨æˆˇå…ąäēĢᅧቇ", "storage": "存储įŠē间", "storage_label": "å­˜å‚¨æ ‡į­ž", @@ -2203,16 +2207,17 @@ "supporter": "čĩžåŠŠč€…", "swap_merge_direction": "äē’æĸ合åšļ斚向", "sync": "同æ­Ĩ", - "sync_albums": "同æ­Ĩį›¸å†Œ", - "sync_albums_manual_subtitle": "将所有上äŧ įš„视éĸ‘å’Œį…§į‰‡åŒæ­Ĩåˆ°é€‰åŽšįš„å¤‡äģŊį›¸å†Œ", + "sync_albums": "同æ­Ĩᛏį°ŋ", + "sync_albums_manual_subtitle": "将所有上äŧ įš„视éĸ‘å’Œį…§į‰‡åŒæ­Ĩåˆ°é€‰åŽšįš„å¤‡äģŊᛏį°ŋ", "sync_local": "同æ­ĨæœŦ地", "sync_remote": "同æ­Ĩčŋœį¨‹", "sync_status": "同æ­ĨįŠļ态", "sync_status_subtitle": "æŸĨįœ‹å’ŒįŽĄį†åŒæ­ĨįŗģįģŸ", - "sync_upload_album_setting_subtitle": "创åģēį…§į‰‡å’Œč§†éĸ‘åšļ上äŧ åˆ° Immich ä¸Šįš„é€‰åŽšį›¸å†Œä¸­", + "sync_upload_album_setting_subtitle": "创åģēį…§į‰‡å’Œč§†éĸ‘åšļ上äŧ åˆ° Immich ä¸Šé€‰åŽšįš„į›¸į°ŋ", "tag": "æ ‡į­ž", "tag_assets": "æ ‡čŽ°éĄšį›Ž", "tag_created": "åˇ˛åˆ›åģēæ ‡į­žīŧš{tag}", + "tag_face": "æ ‡čŽ°äēē脸", "tag_feature_description": "按é€ģčž‘æ ‡į­žåˆ†įģ„åšļæĩč§ˆį…§į‰‡å’Œč§†éĸ‘", "tag_not_found_question": "æ‰žä¸åˆ°æ ‡į­žå—īŧŸåˆ›åģēæ–°æ ‡į­žã€‚", "tag_people": "å‘Ŋ名äēēį‰Š", @@ -2287,7 +2292,7 @@ "unable_to_setup_pin_code": "æ— æŗ•čŽžįŊŽPIN᠁", "unarchive": "取æļˆåŊ’æĄŖ", "unarchive_action_prompt": "厞äģŽåŊ’æĄŖä¸­į§ģ除 {count} 饚", - "unarchived_count": "{count, plural, other {取æļˆåŊ’æĄŖ # 饚}}", + "unarchived_count": "{count, plural, other {取æļˆåŊ’æĄŖ#饚}}", "undo": "撤销", "unfavorite": "取æļˆæ”ļ藏", "unfavorite_action_prompt": "厞äģŽæ”ļ藏中į§ģ除 {count} 饚", @@ -2301,22 +2306,22 @@ "unlink_oauth": "č§Ŗįģ‘ OAuth", "unlinked_oauth_account": "č§Ŗįģ‘ OAuth č´Ļæˆˇ", "unmute_memories": "取æļˆé™éŸŗå›žåŋ†", - "unnamed_album": "æœĒå‘Ŋåį›¸å†Œ", - "unnamed_album_delete_confirmation": "æ‚¨įĄŽåŽščĻåˆ é™¤č¯Ĩį›¸å†Œå—īŧŸ", + "unnamed_album": "æœĒå‘Ŋåį›¸į°ŋ", + "unnamed_album_delete_confirmation": "æ‚¨įĄŽåŽščĻåˆ é™¤č¯Ĩᛏį°ŋ吗īŧŸ", "unnamed_share": "æœĒå‘Ŋåå…ąäēĢ", "unsaved_change": "äŋŽæ”šæœĒäŋå­˜", "unselect_all": "取æļˆå…¨é€‰", "unselect_all_duplicates": "取æļˆé€‰æ‹Šæ‰€æœ‰é‡å¤éĄš", "unselect_all_in": "取æļˆé€‰æ‹Š {group} ä¸­įš„æ‰€æœ‰å†…åŽš", "unstack": "取æļˆå †å ", - "unstack_action_prompt": "{count} ä¸ĒæœĒ堆叠", + "unstack_action_prompt": "{count}ä¸ĒæœĒ堆叠", "unstacked_assets_count": "{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}åˇ˛å–æļˆå †å ", "unsupported_field_type": "ä¸æ”¯æŒįš„å­—æŽĩįąģ型", "unsupported_file_type": "不支持上äŧ æ–‡äģļ {file}īŧŒåŊ“前不支持 {type} įąģåž‹įš„æ–‡äģļ。", "untagged": "æ— æ ‡į­ž", "untitled_workflow": "无标éĸ˜åˇĨäŊœæĩ", "up_next": "下一ä¸Ē", - "update_location_action_prompt": "更新 {count} ä¸Ē所选čĩ„äē§įš„äŊįŊŽīŧš", + "update_location_action_prompt": "更新{count}ä¸Ēæ‰€é€‰éĄšį›Žįš„äŊįŊŽīŧš", "updated_at": "最后更新æ—ļ间", "updated_password": "æ›´æ–°å¯†į ", "upload": "上äŧ ", @@ -2333,7 +2338,7 @@ "upload_status_errors": "错蝝", "upload_status_uploaded": "厞䏊äŧ ", "upload_success": "上äŧ æˆåŠŸīŧŒåˆˇæ–°éĄĩéĸæŸĨįœ‹æ–°ä¸Šäŧ įš„éĄšį›Žã€‚", - "upload_to_immich": "上äŧ č‡ŗ Immichīŧˆ{count}īŧ‰", + "upload_to_immich": "上äŧ č‡ŗImmichīŧˆ{count}īŧ‰", "uploading": "æ­Ŗåœ¨ä¸Šäŧ ", "uploading_media": "文äģļ上äŧ ä¸­", "url": "URL", @@ -2346,7 +2351,7 @@ "user": "į”¨æˆˇ", "user_has_been_deleted": "æ­¤į”¨æˆˇåˇ˛čĸĢ删除。", "user_id": "į”¨æˆˇ ID", - "user_liked": "“{user}â€į‚ščĩžäē†{type, select, photo {č¯Ĩᅧቇ} video {č¯Ĩ视éĸ‘} asset {č¯ĨéĄšį›Ž} other {厃}}", + "user_liked": "{user}į‚ščĩžäē†{type, select, photo {č¯Ĩᅧቇ} video {č¯Ĩ视éĸ‘} asset {č¯ĨéĄšį›Ž} other {厃}}", "user_pin_code_settings": "PIN᠁", "user_pin_code_settings_description": "įŽĄį†äŊ įš„PIN᠁", "user_privacy": "į”¨æˆˇéšį§", @@ -2358,7 +2363,7 @@ "user_usage_stats_description": "æŸĨįœ‹å¸æˆˇäŊŋᔍįģŸčŽĄäŋĄæ¯", "username": "į”¨æˆˇå", "users": "į”¨æˆˇ", - "users_added_to_album_count": "厞将 {count, plural, one {# ä¸Ēį”¨æˆˇ} other {# ä¸Ēį”¨æˆˇ}} æˇģåŠ åˆ°į›¸å†Œ", + "users_added_to_album_count": "厞将 {count, plural, one {# ä¸Ēį”¨æˆˇ} other {# ä¸Ēį”¨æˆˇ}} æˇģåŠ åˆ°į›¸į°ŋ", "utilities": "åŽžį”¨åˇĨå…ˇ", "validate": "énj蝁", "validate_endpoint_error": "č¯ˇčž“å…Ĩæœ‰æ•ˆįš„ URL", @@ -2366,7 +2371,7 @@ "variables": "变量", "version": "į‰ˆæœŦ", "version_announcement_closing": "æ‚¨įš„æœ‹å‹īŧŒAlex", - "version_announcement_message": "Immich įŽ°åˇ˛æŽ¨å‡ēæ–°į‰ˆæœŦã€‚č¯ˇæŸĨé˜…å‘čĄŒč¯´æ˜ŽīŧŒåŠæ—ļ更新配įŊŽäģĨ防æ­ĸå‡ē错。č‹Ĩ您通čŋ‡ WatchTower 或å…ļäģ–åˇĨå…ˇč‡Ē动更新 ImmichīŧŒéœ€į‰šåˆĢæŗ¨æ„ã€‚", + "version_announcement_message": "äŊ åĨŊīŧImmich įš„æ–°į‰ˆæœŦ厞įģå‘å¸ƒã€‚č¯ˇčŠąį‚šæ—ļ间阅č¯ģå‘čĄŒč¯´æ˜ŽīŧŒäģĨįĄŽäŋäŊ įš„部įŊ˛įޝåĸƒäŋæŒæœ€æ–°īŧŒäģŽč€Œéŋ免配įŊŽé”™č¯¯ã€‚į‰šåˆĢ是åĻ‚æžœäŊ äŊŋᔍäē† WatchTower 或å…ļäģ–č‡Ē动更新æœēåˆļīŧŒčŋ™ä¸€į‚šå°¤ä¸ē重čĻã€‚", "version_history": "į‰ˆæœŦæ›´æ–°åŽ†å˛čŽ°åŊ•", "version_history_item": "在 {date} åŽ‰čŖ… {version} į‰ˆæœŦ", "video": "视éĸ‘", @@ -2376,10 +2381,10 @@ "videos_count": "{count, plural, one {#ä¸Ē视éĸ‘} other {#ä¸Ē视éĸ‘}}", "videos_only": "äģ…视éĸ‘", "view": "æŸĨįœ‹", - "view_album": "æŸĨįœ‹į›¸å†Œ", + "view_album": "æŸĨįœ‹į›¸į°ŋ", "view_all": "æŸĨįœ‹å…¨éƒ¨", "view_all_users": "æŸĨįœ‹å…¨éƒ¨į”¨æˆˇ", - "view_asset_owners": "æŸĨįœ‹čĩ„äē§æ‰€æœ‰č€…", + "view_asset_owners": "æŸĨįœ‹éĄšį›Žæ‰€æœ‰č€…", "view_details": "æŸĨįœ‹č¯Ļ情", "view_in_timeline": "在æ—ļ间čŊ´ä¸­æŸĨįœ‹", "view_link": "æŸĨįœ‹é“žæŽĨ", @@ -2392,8 +2397,9 @@ "view_stack": "æŸĨįœ‹å †å éĄšį›Ž", "view_user": "æŸĨįœ‹į”¨æˆˇ", "viewer_remove_from_stack": "äģŽå †å ä¸­į§ģ除", - "viewer_stack_use_as_main_asset": "äŊœä¸ēä¸ģéĄšį›ŽäŊŋᔍ", + "viewer_stack_use_as_main_asset": "äŊœä¸ēä¸ģį”ģ像äŊŋᔍ", "viewer_unstack": "取æļˆå †å ", + "visibility": "å¯č§æ€§", "visibility_changed": "{count, plural, one {#ä¸Ēäēēį‰Š} other {#ä¸Ēäēēį‰Š}}įš„å¯č§æ€§åˇ˛äŋŽæ”š", "visual": "å¯č§†åŒ–", "visual_builder": "å¯č§†åŒ–į”Ÿæˆå™¨", @@ -2404,7 +2410,7 @@ "welcome": "æŦĸčŋŽ", "welcome_to_immich": "æŦĸčŋŽäŊŋᔍ Immich", "width": "åŽŊåēĻ", - "wifi_name": "Wi-Fi åį§°", + "wifi_name": "Wi-Fiåį§°", "workflow_delete_prompt": "æ‚¨įĄŽåŽščĻåˆ é™¤æ­¤åˇĨäŊœæĩå—īŧŸ", "workflow_deleted": "åˇĨäŊœæĩåˇ˛åˆ é™¤", "workflow_description": "åˇĨäŊœæĩæčŋ°", @@ -2424,7 +2430,7 @@ "yes": "是", "you_dont_have_any_shared_links": "æ‚¨æ˛Ąæœ‰äģģäŊ•å…ąäēĢ链æŽĨ", "your_wifi_name": "æ‚¨įš„ Wi-Fi åį§°", - "zero_to_clear_rating": "按0清除čĩ„äē§æ˜Ÿįē§", + "zero_to_clear_rating": "按0æ¸…é™¤éĄšį›Žæ˜Ÿįē§", "zoom_image": "įŧŠæ”žå›žåƒ", "zoom_to_bounds": "įŧŠæ”žåˆ°čžšį•Œ" } diff --git a/i18n/zh_Hant.json b/i18n/zh_Hant.json index a61b03c28c..00b9629264 100644 --- a/i18n/zh_Hant.json +++ b/i18n/zh_Hant.json @@ -54,7 +54,7 @@ "authentication_settings_description": "įŽĄį†å¯†įĸŧ、OAuth 與å…ļäģ–éŠ—č­‰č¨­åŽš", "authentication_settings_disable_all": "您įĸē厚čĻåœį”¨æ‰€æœ‰į™ģå…Ĩæ–šåŧå—ŽīŧŸé€™å°‡å°Žč‡´åŽŒå…¨į„Ąæŗ•į™ģå…Ĩ。", "authentication_settings_reenable": "åĻ‚éœ€é‡æ–°å•Ÿį”¨īŧŒčĢ‹äŊŋᔍ äŧ翜å™¨æŒ‡äģ¤ã€‚", - "background_task_job": "čƒŒæ™¯åˇĨäŊœ", + "background_task_job": "čƒŒæ™¯äģģ務", "backup_database": "åģēįĢ‹čŗ‡æ–™åēĢ備äģŊ", "backup_database_enable_description": "å•Ÿį”¨čŗ‡æ–™åēĢ備äģŊ", "backup_keep_last_amount": "äŋį•™å…ˆå‰å‚™äģŊįš„æ•¸é‡", @@ -63,7 +63,7 @@ "backup_onboarding_3_description": "æ‚¨čŗ‡æ–™įš„į¸Ŋ備äģŊäģŊ數īŧŒåŒ…æ‹Ŧ原始æĒ”æĄˆåœ¨å…§ã€‚é€™åŒ…æ‹Ŧ 1 äģŊį•°åœ°å‚™äģŊ與 2 äģŊæœŦ抟副æœŦ。", "backup_onboarding_description": "åģēč­°æŽĄį”¨ 3-2-1 備äģŊį­–į•Ĩ 來äŋč­ˇæ‚¨įš„čŗ‡æ–™ã€‚æ‚¨æ‡‰äŋį•™åˇ˛ä¸Šå‚ŗįš„ᛏቇ/åŊąį‰‡å‰¯æœŦīŧŒäģĨ及 Immich čŗ‡æ–™åēĢīŧŒäģĨåģēįĢ‹åŽŒæ•´įš„å‚™äģŊæ–šæĄˆã€‚", "backup_onboarding_footer": "更多備äģŊ Immich čŗ‡č¨ŠīŧŒčĢ‹åƒč€ƒ čĒĒæ˜Žæ–‡äģļ。", - "backup_onboarding_parts_title": "éĩåžžå‚™äģŊ原則 3-2-1īŧš", + "backup_onboarding_parts_title": "3-2-1 備äģŊ包åĢīŧš", "backup_onboarding_title": "備äģŊ", "backup_settings": "čŗ‡æ–™åēĢ備äģŊč¨­åŽš", "backup_settings_description": "įŽĄį†čŗ‡æ–™åēĢ備äģŊč¨­åŽšã€‚", @@ -81,20 +81,20 @@ "cron_expression_description": "äŊŋᔍ Cron æ ŧåŧč¨­åŽšæŽƒæé–“éš”ã€‚æ›´å¤ščŗ‡č¨ŠčĢ‹åƒé–ą Crontab Guru", "cron_expression_presets": "Cron 表達åŧé č¨­å€ŧ", "disable_login": "åœį”¨į™ģå…Ĩ", - "duplicate_detection_job_description": "䞝靠æ™ēæ…§æœå°‹ã€‚å°é …į›ŽåŸˇčĄŒæŠŸå™¨å­¸įŋ’䞆åĩæ¸Ŧᛏäŧŧåœ–į‰‡", + "duplicate_detection_job_description": "é‡å°čŗ‡į”ĸåŸˇčĄŒæŠŸå™¨å­¸įŋ’äģĨåĩæ¸Ŧᛏäŧŧåœ–į‰‡ã€‚éœ€äžčŗ´ã€Œæ™ē慧搜尋」功čƒŊ", "exclusion_pattern_description": "æŽ’é™¤æ¨Ąåŧå¯čŽ“æ‚¨åœ¨æŽƒæåĒ’éĢ”åēĢæ™‚åŋŊį•Ĩį‰šåŽšæĒ”æĄˆčˆ‡čŗ‡æ–™å¤žã€‚č‹Ĩ某äē›čŗ‡æ–™å¤žåŒ…åĢæ‚¨ä¸æƒŗåŒ¯å…Ĩįš„æĒ”æĄˆīŧˆäž‹åĻ‚ RAW æĒ”īŧ‰īŧŒæ­¤åŠŸčƒŊå°‡éžå¸¸æœ‰į”¨ã€‚", "export_config_as_json_description": "å°‡į›Žå‰įŗģįĩąč¨­åŽšä¸‹čŧ‰į‚ē JSON æĒ”æĄˆ", "external_libraries_page_description": "įŽĄį†å¤–éƒ¨åĒ’éĢ”åēĢ頁éĸ", "face_detection": "臉孔åĩæ¸Ŧ", - "face_detection_description": "äŊŋį”¨æŠŸå™¨å­¸įŋ’åĩæ¸Ŧé …į›Žä¸­įš„č‡‰å­”ã€‚å°æ–ŧåŊąį‰‡īŧŒåƒ…æœƒåˆ†æžį¸Žåœ–ã€‚ã€Œé‡æ–°æ•´į†ã€æœƒé‡æ–°č™•į†æ‰€æœ‰é …į›Žīŧ›ã€Œé‡č¨­ã€å‰‡æœƒéĄå¤–æ¸…é™¤į›Žå‰įš„č‡‰å­”čŗ‡æ–™īŧ›ã€ŒåŠ å…ĨæŽ’į¨‹ã€æœƒå°‡å°šæœĒč™•į†įš„é …į›ŽåŠ å…Ĩåēåˆ—ã€‚åŽŒæˆã€Œč‡‰å­”åĩæ¸Ŧ」垌īŧŒåĩæ¸Ŧåˆ°įš„č‡‰å­”å°‡åŠ å…Ĩã€Œč‡‰å­”čž¨č­˜ã€æŽ’į¨‹īŧŒä¸Ļæ­¸éĄžč‡ŗįžæœ‰æˆ–æ–°įš„äēēį‰Šįž¤įĩ„。", - "facial_recognition_job_description": "將åĩæ¸Ŧåˆ°įš„č‡‰å­”æ­¸éĄžį‚ēäēēį‰Šã€‚æ­¤æ­ĨéŠŸæœƒåœ¨č‡‰å­”åĩæ¸ŦåŽŒæˆåžŒåŸˇčĄŒã€‚ã€Œé‡č¨­ã€æœƒé‡æ–°å°æ‰€æœ‰č‡‰å­”é€˛čĄŒåˆ†įž¤īŧ›ã€ŒåŠ å…ĨæŽ’į¨‹ã€å‰‡æœƒå°‡å°šæœĒ指洞äēēį‰Šįš„č‡‰å­”åŠ å…Ĩåēåˆ—。", + "face_detection_description": "äŊŋį”¨æŠŸå™¨å­¸įŋ’åĩæ¸Ŧé …į›Žä¸­įš„č‡‰å­”ã€‚å°æ–ŧåŊąį‰‡īŧŒåƒ…æœƒåˆ†æžį¸Žåœ–ã€‚ã€Œé‡æ–°æ•´į†ã€æœƒé‡æ–°č™•į†æ‰€æœ‰é …į›Žīŧ›ã€Œé‡č¨­ã€å‰‡æœƒéĄå¤–æ¸…é™¤į›Žå‰įš„č‡‰å­”čŗ‡æ–™īŧ›ã€ŒåŠ å…ĨæŽ’į¨‹ã€æœƒå°‡å°šæœĒč™•į†įš„é …į›ŽåŠ å…ĨäŊ‡åˆ—ã€‚åŽŒæˆã€Œč‡‰å­”åĩæ¸Ŧ」垌īŧŒåĩæ¸Ŧåˆ°įš„č‡‰å­”å°‡åŠ å…Ĩã€Œč‡‰å­”čž¨č­˜ã€æŽ’į¨‹īŧŒä¸Ļæ­¸éĄžč‡ŗįžæœ‰æˆ–æ–°įš„äēēį‰Šįž¤įĩ„。", + "facial_recognition_job_description": "將åĩæ¸Ŧåˆ°įš„č‡‰å­”æ­¸éĄžį‚ēäēēį‰Šã€‚æ­¤æ­ĨéŠŸæœƒåœ¨č‡‰å­”åĩæ¸ŦåŽŒæˆåžŒåŸˇčĄŒã€‚ã€Œé‡č¨­ã€æœƒé‡æ–°å°æ‰€æœ‰č‡‰å­”é€˛čĄŒåˆ†įž¤īŧ›ã€ŒåŠ å…ĨæŽ’į¨‹ã€å‰‡æœƒå°‡å°šæœĒ指洞äēēį‰Šįš„č‡‰å­”åŠ å…ĨäŊ‡åˆ—。", "failed_job_command": "{job} äģģå‹™įš„ {command} 指äģ¤åŸˇčĄŒå¤ąæ•—", "force_delete_user_warning": "č­Ļ告īŧšé€™å°‡įĢ‹åŗåˆĒ除äŊŋį”¨č€…åŠå…￉€æœ‰é …į›Žã€‚æ­¤å‹•äŊœį„Ąæŗ•垊原īŧŒä¸”į„Ąæŗ•æ‰žå›žåˇ˛åˆĒé™¤įš„æĒ”æĄˆã€‚", "image_format": "æ ŧåŧ", "image_format_description": "WebP čƒŊį”ĸį”Ÿį›¸å°æ–ŧ JPEG æ›´å°įš„æĒ”æĄˆīŧŒäŊ†įˇ¨įĸŧ速åēĻčŧƒæ…ĸ。", "image_fullsize_description": "į§ģ除中įšŧčŗ‡æ–™įš„å¤§å°ē寸åŊąåƒīŧŒåœ¨æ”žå¤§åœ–į‰‡æ™‚äŊŋᔍ", "image_fullsize_enabled": "å•Ÿį”¨å¤§å°ē寸åŊąåƒį”ĸį”Ÿ", - "image_fullsize_enabled_description": "į‚ē非įļ˛é å‹å–„æ ŧåŧį”ĸį”Ÿå¤§å°ēå¯¸į›¸į‰‡ã€‚å•Ÿį”¨ã€ŒååĨŊ內åĩŒé čĻŊ」時īŧŒįŗģįĩąå°‡į›´æŽĨäŊŋᔍ內åĩŒé čĻŊ而不進行čŊ‰įĸŧīŧŒä¸åŊąéŸŋ JPEG į­‰įļ˛é å‹å–„æ ŧåŧã€‚", + "image_fullsize_enabled_description": "į‚ē非įļ˛é į›¸åŽšæ ŧåŧį”ĸį”Ÿå¤§å°ēå¯¸į›¸į‰‡ã€‚å•Ÿį”¨ã€ŒååĨŊ內åĩŒé čĻŊ」時īŧŒįŗģįĩąå°‡į›´æŽĨäŊŋᔍ內åĩŒé čĻŊ而不進行čŊ‰įĸŧīŧŒä¸åŊąéŸŋ JPEG į­‰įļ˛é į›¸åŽšæ ŧåŧã€‚", "image_fullsize_quality_description": "大å°ē寸åŊąåƒå“čŗĒīŧŒį¯„圍į‚ē 1 到 100。數å€ŧčļŠéĢ˜å“čŗĒčļŠåĨŊīŧŒäŊ†æĒ”æĄˆä🿜ƒčļŠå¤§ã€‚", "image_fullsize_title": "大å°ē寸åŊąåƒč¨­åޚ", "image_prefer_embedded_preview": "偏åĨŊ內åĩŒé čĻŊ", @@ -104,14 +104,14 @@ "image_preview_description": "䏭ᭉå°ē寸åŊąåƒīŧˆä¸åĢ中įšŧčŗ‡æ–™īŧ‰īŧŒį”¨æ–ŧæĒĸčĻ–å–Žä¸€é …į›Žčˆ‡æŠŸå™¨å­¸įŋ’", "image_preview_quality_description": "預čĻŊ品čŗĒį¯„åœį‚ē 1 到 100。數å€ŧčļŠéĢ˜å“čŗĒčļŠåĨŊīŧŒäŊ†æĒ”æĄˆä🿜ƒæ›´å¤§īŧŒä¸Ļ可čƒŊ降äŊŽæ‡‰į”¨į¨‹åŧįš„回應速åēĻã€‚č¨­åŽšéŽäŊŽįš„æ•¸å€ŧ可čƒŊ會åŊąéŸŋ抟器學įŋ’įš„å“čŗĒ。", "image_preview_title": "預čĻŊč¨­åŽš", - "image_progressive": "逐æ­Ĩ", - "image_progressive_description": "對 JPEG åŊąåƒé€˛čĄŒæŧ¸é€˛åŧįˇ¨įĸŧīŧŒäģĨå¯Ļįžæŧ¸é€˛åŧčŧ‰å…ĨéĄ¯į¤ē。這不會åŊąéŸŋ WebP åŊąåƒã€‚", + "image_progressive": "æŧ¸é€˛åŧ", + "image_progressive_description": "對 JPEG åŊąåƒé€˛čĄŒæŧ¸é€˛åŧįˇ¨įĸŧīŧŒäģĨ達成æŧ¸é€˛åŧčŧ‰å…ĨéĄ¯į¤ē。這不會åŊąéŸŋ WebP åŊąåƒã€‚", "image_quality": "品čŗĒ", "image_resolution": "č§ŖæžåēĻ", "image_resolution_description": "čŧƒéĢ˜įš„č§ŖæžåēĻčƒŊäŋį•™æ›´å¤šį´°į¯€īŧŒäŊ†įˇ¨įĸŧæ™‚é–“æœƒæ›´é•ˇã€æĒ”æĄˆå¤§å°æœƒæ›´å¤§īŧŒä¸Ļ可čƒŊ降äŊŽæ‡‰į”¨į¨‹åŧįš„回應速åēĻ。", "image_settings": "åœ–į‰‡č¨­åŽš", "image_settings_description": "įŽĄį†į”ĸį”Ÿįš„åŊąåƒå“čŗĒčˆ‡č§ŖæžåēĻ", - "image_thumbnail_description": "į§ģ除中įšŧčŗ‡æ–™įš„å°åž‹į¸Žåœ–īŧŒäģĨᔍæ–ŧæĒĸčĻ–å¤§é‡į›¸į‰‡æ™‚äŊŋᔍīŧŒäž‹åĻ‚ä¸ģ時間čģ¸", + "image_thumbnail_description": "厞į§ģ除中įšŧčŗ‡æ–™įš„å°åž‹į¸Žåœ–īŧŒį”¨æ–ŧæĒĸčĻ–å¤šåŧĩᛏቇīŧˆåĻ‚ä¸ģ時間čģ¸īŧ‰", "image_thumbnail_quality_description": "į¸Žåœ–å“čŗĒį¯„åœį‚ē 1 到 100。數å€ŧčļŠéĢ˜å“čŗĒčļŠåĨŊīŧŒäŊ†æĒ”æĄˆä🿜ƒæ›´å¤§īŧŒä¸Ļ可čƒŊ降äŊŽæ‡‰į”¨į¨‹åŧįš„回應速åēĻ。", "image_thumbnail_title": "į¸Žåœ–č¨­åŽš", "import_config_from_json_description": "é€éŽä¸Šå‚ŗ JSON č¨­åŽšæĒ”匯å…Ĩįŗģįĩąč¨­åޚ", @@ -160,7 +160,7 @@ "machine_learning_facial_recognition": "äēē臉辨識", "machine_learning_facial_recognition_description": "åĩæ¸Ŧã€čž¨č­˜ä¸Ļå°åœ–į‰‡ä¸­įš„č‡‰å­”åˆ†éĄž", "machine_learning_facial_recognition_model": "äēēč‡‰čž¨č­˜æ¨Ąåž‹", - "machine_learning_facial_recognition_model_description": "æ¨Ąåž‹é †åēį”ąå¤§č‡ŗå°æŽ’列。čŧƒå¤§įš„æ¨Ąåž‹é€ŸåēĻčŧƒæ…ĸ且äŊ”ᔍčŧƒå¤šč¨˜æ†ļéĢ”īŧŒäŊ†æ•ˆæžœčŧƒäŊŗã€‚čĢ‹æŗ¨æ„īŧŒæ›´æ›æ¨Ąåž‹åžŒåŋ…須對所有åŊąåƒé‡æ–°åŸˇčĄŒã€Œč‡‰å­”åĩæ¸Ŧ」äģģ務。", + "machine_learning_facial_recognition_model_description": "æ¨Ąåž‹é †åēį”ąå¤§č‡ŗå°æŽ’列。čŧƒå¤§įš„æ¨Ąåž‹é€ŸåēĻčŧƒæ…ĸ且äŊ”ᔍčŧƒå¤šč¨˜æ†ļéĢ”īŧŒäŊ†įĩæžœčŧƒäŊŗã€‚čĢ‹æŗ¨æ„īŧŒæ›´æ›æ¨Ąåž‹åžŒåŋ…須對所有åŊąåƒé‡æ–°åŸˇčĄŒã€Œč‡‰å­”åĩæ¸Ŧ」äģģ務。", "machine_learning_facial_recognition_setting": "å•Ÿį”¨äēē臉辨識", "machine_learning_facial_recognition_setting_description": "č‹Ĩåœį”¨īŧŒåŊąåƒå°‡ä¸æœƒé€˛čĄŒäēēč‡‰čž¨č­˜įˇ¨įĸŧīŧŒä¸”「æŽĸį´ĸ」頁éĸįš„ã€Œäēēį‰Šã€å€åĄŠå°‡ä¸æœƒéĄ¯į¤ēäģģäŊ•內厚。", "machine_learning_max_detection_distance": "åĩæ¸Ŧ距é›ĸ上限", @@ -173,7 +173,7 @@ "machine_learning_min_recognized_faces_description": "åģēįĢ‹æ–°äēēį‰Šæ‰€éœ€įš„æœ€äŊŽåˇ˛čž¨č­˜č‡‰å­”數量。提éĢ˜æ­¤æ•¸å€ŧå¯čŽ“č‡‰å­”čž¨č­˜æ›´į˛žįĸēīŧŒäŊ†åŒæ™‚會åĸžåР臉孔æœĒčĸĢæŒ‡æ´žįĩĻäģģäŊ•äēēį‰Šįš„å¯čƒŊ性。", "machine_learning_ocr": "æ–‡å­—čž¨č­˜(OCR)", "machine_learning_ocr_description": "äŊŋį”¨æŠŸå™¨å­¸įŋ’čž¨č­˜åŊąåƒä¸­įš„æ–‡å­—", - "machine_learning_ocr_enabled": "å•Ÿį”¨OCR", + "machine_learning_ocr_enabled": "å•Ÿį”¨ OCR", "machine_learning_ocr_enabled_description": "č‹Ĩåœį”¨īŧŒåŊąåƒå°‡ä¸æœƒé€˛čĄŒæ–‡å­—čž¨č­˜ã€‚", "machine_learning_ocr_max_resolution": "æœ€å¤§č§ŖæžåēĻ", "machine_learning_ocr_max_resolution_description": "č§ŖæžåēĻé̘æ–ŧæ­¤å€ŧįš„é čĻŊåŊąåƒå°‡åœ¨äŋæŒé•ˇå¯Ŧæ¯”įš„æƒ…æŗä¸‹čĒŋ整大小。數å€ŧčļŠé̘čē–įĸēīŧŒäŊ†č™•į†æ™‚é–“æ›´é•ˇä¸”æœƒäŊ”į”¨æ›´å¤šč¨˜æ†ļéĢ”ã€‚", @@ -181,7 +181,7 @@ "machine_learning_ocr_min_detection_score_description": "文字åĩæ¸Ŧįš„æœ€äŊŽäŋĄåŋƒåˆ†æ•¸īŧŒį¯„圍į‚ē 0 - 1。čŧƒäŊŽįš„æ•¸å€ŧ會åĩæ¸Ŧ到更多文字īŧŒäŊ†å¯čƒŊå°Žč‡´čĒ¤åˆ¤ã€‚", "machine_learning_ocr_min_recognition_score": "最äŊŽčž¨č­˜åˆ†æ•¸", "machine_learning_ocr_min_score_recognition_description": "厞åĩæ¸Ŧæ–‡å­—įš„æœ€äŊŽčž¨č­˜äŋĄåŋƒåˆ†æ•¸īŧŒį¯„圍į‚ē 0 - 1。čŧƒäŊŽįš„æ•¸å€ŧæœƒčž¨č­˜å‡ē更多文字īŧŒäŊ†å¯čƒŊå°Žč‡´čĒ¤åˆ¤ã€‚", - "machine_learning_ocr_model": "OCRæ¨Ąåž‹", + "machine_learning_ocr_model": "OCR æ¨Ąåž‹", "machine_learning_ocr_model_description": "äŧ翜å™¨æ¨Ąåž‹æ¯”čĄŒå‹•čŖįŊŽæ¨Ąåž‹æ›´æē–įĸēīŧŒäŊ†č™•į†æ™‚é–“čŧƒé•ˇä¸”會äŊ”į”¨æ›´å¤šč¨˜æ†ļéĢ”ã€‚", "machine_learning_settings": "抟器學įŋ’設åޚ", "machine_learning_settings_description": "įŽĄį†æŠŸå™¨å­¸įŋ’įš„åŠŸčƒŊå’Œč¨­åŽš", @@ -257,7 +257,7 @@ "notification_email_password_description": "ᔍæ–ŧ與é›ģ子éƒĩäģļäŧ翜å™¨éŠ—č­‰įš„å¯†įĸŧ", "notification_email_port_description": "é›ģ子éƒĩäģļäŧ翜å™¨įš„逪æŽĨ埠īŧˆäž‹åĻ‚ 25、465 或 587īŧ‰", "notification_email_secure": "SMTPS", - "notification_email_secure_description": "äŊŋᔍSMTPSīŧˆåŸēæ–ŧTLSįš„SMTPīŧ‰", + "notification_email_secure_description": "äŊŋᔍ SMTPSīŧˆåŸēæ–ŧ TLS įš„ SMTPīŧ‰", "notification_email_sent_test_email_button": "傺送æ¸ŦčŠĻé›ģ子éƒĩäģļä¸Ļå„˛å­˜", "notification_email_setting_description": "寄送é›ģ子éƒĩäģļ通įŸĨįš„č¨­åŽš", "notification_email_test_email": "傺送æ¸ŦčŠĻé›ģ子éƒĩäģļ", @@ -281,11 +281,11 @@ "oauth_role_claim_description": "æ šæ“šæ­¤åŽŖå‘Šįš„å­˜åœ¨īŧŒč‡Ē動授äēˆįŽĄį†å“ĄæŦŠé™ã€‚čŠ˛åŽŖå‘Šįš„å€ŧ可äģĨ是 'user' 或 'admin'。", "oauth_settings": "OAuth", "oauth_settings_description": "įŽĄį† OAuth į™ģå…Ĩč¨­åŽš", - "oauth_settings_more_details": "æŦ˛įž­č§Ŗæ­¤åŠŸčƒŊīŧŒčĢ‹åƒé–ą čĒĒæ˜Žæ›¸ã€‚", + "oauth_settings_more_details": "č‹ĨčĻįž­č§Ŗæ­¤åŠŸčƒŊįš„čŠŗį´°čŗ‡č¨ŠīŧŒčĢ‹åƒé–ą čĒĒæ˜Žæ–‡äģļ。", "oauth_storage_label_claim": "å„˛å­˜æ¨™įą¤åŽŖå‘Š", - "oauth_storage_label_claim_description": "č‡Ē動將äŊŋį”¨č€…įš„å„˛å­˜æ¨™įą¤åŽšį‚ēæ­¤åŽŖå‘Šäš‹å€ŧ。", + "oauth_storage_label_claim_description": "č‡Ē動將äŊŋį”¨č€…įš„å„˛å­˜æ¨™įą¤č¨­åŽšį‚ēæ­¤åŽŖå‘Šäš‹å€ŧ。", "oauth_storage_quota_claim": "å„˛å­˜é…éĄåŽŖå‘Š", - "oauth_storage_quota_claim_description": "č‡Ē動將äŊŋį”¨č€…įš„å„˛å­˜é…éĄåŽšį‚ēæ­¤åŽŖå‘Šäš‹å€ŧ。", + "oauth_storage_quota_claim_description": "č‡Ē動將äŊŋį”¨č€…įš„å„˛å­˜é…éĄč¨­åŽšį‚ēæ­¤åŽŖå‘Šäš‹å€ŧ。", "oauth_storage_quota_default": "é č¨­å„˛å­˜é…éĄīŧˆGiBīŧ‰", "oauth_storage_quota_default_description": "æœĒæäž›åŽŖå‘Šæ™‚æ‰€äŊŋį”¨įš„é…éĄīŧˆGiBīŧ‰ã€‚", "oauth_timeout": "čĢ‹æą‚é€žæ™‚", @@ -297,8 +297,8 @@ "paths_validated_successfully": "æ‰€æœ‰čˇ¯åž‘éŠ—č­‰æˆåŠŸ", "person_cleanup_job": "æ¸…į†äēēį‰Š", "queue_details": "äŊ‡åˆ—čŗ‡č¨Š", - "queues": "äģģå‹™æŽ’į¨‹", - "queues_page_description": "åēåˆ—æŽ’į¨‹įŽĄį†į•Œéĸ", + "queues": "äģģ務äŊ‡åˆ—", + "queues_page_description": "įŽĄį†å“Ąäģģ務äŊ‡åˆ—頁éĸ", "quota_size_gib": "é…éĄå¤§å°īŧˆGiBīŧ‰", "refreshing_all_libraries": "æ­Ŗåœ¨é‡æ–°æ•´į†æ‰€æœ‰åĒ’éĢ”åēĢ", "registration": "įŽĄį†č€…č¨ģ冊", @@ -320,8 +320,8 @@ "server_welcome_message": "æ­ĄčŋŽč¨Šæ¯", "server_welcome_message_description": "在į™ģå…Ĩ頁éĸéĄ¯į¤ēįš„č¨Šæ¯ã€‚", "settings_page_description": "įŽĄį†č¨­åŽšé éĸ", - "sidecar_job": "側æŽĨæĒ”æĄˆä¸­įšŧčŗ‡æ–™", - "sidecar_job_description": "åžžæĒ”æĄˆįŗģįĩąåĩæ¸Ŧ或同æ­Ĩ側æŽĨæĒ”æĄˆä¸­įšŧčŗ‡æ–™", + "sidecar_job": "附åąŦæĒ”æĄˆä¸­įšŧčŗ‡æ–™", + "sidecar_job_description": "åžžæĒ”æĄˆįŗģįĩąåĩæ¸Ŧ或同æ­Ĩ附åąŦæĒ”æĄˆä¸­įšŧčŗ‡æ–™", "slideshow_duration_description": "每åŧĩåœ–į‰‡æ”žæ˜ įš„į§’æ•¸", "smart_search_job_description": "å°é …į›ŽåŸˇčĄŒæŠŸå™¨å­¸įŋ’äģĨ支援æ™ē慧搜尋", "storage_template_date_time_description": "æĒ”æĄˆįš„åģēįĢ‹æ™‚é–“æˆŗæœƒį”¨æ–ŧæ—ĨæœŸčˆ‡æ™‚é–“čŗ‡č¨Š", @@ -411,7 +411,7 @@ "transcoding_tone_mapping": "色čĒŋ對映", "transcoding_tone_mapping_description": "在將 HDR åŊąį‰‡čŊ‰æ›į‚ē SDR 時īŧŒį›Ąé‡įļ­æŒåŽŸå§‹č§€æ„Ÿã€‚æ¯į¨Žæŧ”įŽ—æŗ•åœ¨č‰˛åŊŠã€į´°į¯€å’ŒäēŽåēĻæ–šéĸéƒŊæœ‰ä¸åŒįš„æŦŠčĄĄã€‚Hable äŋį•™į´°į¯€īŧŒMobius äŋį•™č‰˛åŊŠīŧŒReinhard äŋį•™äēŽåēĻ。", "transcoding_transcode_policy": "čŊ‰įĸŧį­–į•Ĩ", - "transcoding_transcode_policy_description": "åŊąį‰‡čŊ‰įĸŧį­–į•Ĩ。HDR åŊąį‰‡ä¸€åž‹æœƒé€˛čĄŒčŊ‰įĸŧīŧˆé™¤éžåœį”¨čŊ‰įĸŧ功čƒŊīŧ‰ã€‚", + "transcoding_transcode_policy_description": "åŊąį‰‡čŊ‰įĸŧį­–į•Ĩ。HDR åŊąį‰‡å’Œåƒį´ æ ŧåŧä¸æ˜¯ YUV 4:2:0 įš„åŊąį‰‡ä¸€åž‹æœƒé€˛čĄŒčŊ‰įĸŧīŧˆé™¤éžåœį”¨čŊ‰įĸŧ功čƒŊīŧ‰ã€‚", "transcoding_two_pass_encoding": "兊階æŽĩᎍįĸŧ", "transcoding_two_pass_encoding_setting_description": "åŸˇčĄŒå…ŠæŦĄįˇ¨įĸŧäģĨį”ĸį”Ÿå“čŗĒ更äŊŗįš„åŊąį‰‡ã€‚å•Ÿį”¨æœ€å¤§äŊå…ƒé€ŸįŽ‡æ™‚īŧˆH.264 與 HEVC åŋ…é ˆå•Ÿį”¨īŧ‰īŧŒæ­¤æ¨Ąåŧæœƒäžæœ€å¤§äŊå…ƒé€ŸįއčĒŋæ•´į¯„åœä¸ĻåŋŊį•Ĩ CRF。č‹Ĩį‚ē VP9īŧŒå‰‡å¯åœ¨åœį”¨æœ€å¤§äŊå…ƒé€ŸįŽ‡æ™‚äŊŋᔍ CRF。", "transcoding_video_codec": "åŊąį‰‡įˇ¨č§Ŗįĸŧ器", @@ -428,8 +428,8 @@ "user_delete_delay": "{user} įš„å¸ŗč™Ÿå’Œé …į›Žæœƒåœ¨ {delay, plural, one {# 夊} other {# 夊}} 垌永䚅åˆĒ除。", "user_delete_delay_settings": "åģļ垌åˆĒ除", "user_delete_delay_settings_description": "č‡Ēį§ģ除垌čĩˇįŽ—įš„å¤Šæ•¸īŧŒé€žæœŸåžŒå°‡æ°¸äš…åˆĒ除äŊŋį”¨č€…å¸ŗč™Ÿčˆ‡é …į›Žã€‚äŊŋᔍ者åˆĒ除äŊœæĨ­æœƒåœ¨æ¯æ—Ĩåˆå¤œåŸˇčĄŒīŧŒäģĨæĒĸæŸĨįŦĻ合åˆĒ除æĸäģļįš„å¸ŗč™Ÿã€‚æ­¤č¨­åŽšįš„čŽŠæ›´å°‡åœ¨ä¸‹ä¸€æŦĄåŸˇčĄŒæ™‚į”Ÿæ•ˆã€‚", - "user_delete_immediately": "{user} įš„å¸ŗč™Ÿčˆ‡é …į›Žå°‡ įĢ‹åŗ 排å…Ĩ永䚅åˆĒ除åēåˆ—。", - "user_delete_immediately_checkbox": "įĢ‹åŗå°‡äŊŋį”¨č€…čˆ‡é …į›ŽæŽ’å…Ĩ永䚅åˆĒ除åēåˆ—", + "user_delete_immediately": "{user} įš„å¸ŗč™Ÿčˆ‡é …į›Žå°‡ įĢ‹åŗ 排å…Ĩ永䚅åˆĒ除äŊ‡åˆ—。", + "user_delete_immediately_checkbox": "įĢ‹åŗå°‡äŊŋį”¨č€…čˆ‡é …į›ŽæŽ’å…Ĩ永䚅åˆĒ除äŊ‡åˆ—", "user_details": "äŊŋį”¨č€…čŠŗį´°čŗ‡č¨Š", "user_management": "äŊŋį”¨č€…įŽĄį†", "user_password_has_been_reset": "äŊŋᔍ者坆įĸŧåˇ˛é‡č¨­īŧš", @@ -441,7 +441,7 @@ "user_successfully_removed": "åˇ˛æˆåŠŸåˆĒ除äŊŋᔍ者 {email}。", "users_page_description": "įŽĄį†äŊŋᔍ者頁éĸ", "version_check_enabled_description": "å•Ÿį”¨į‰ˆæœŦæĒĸæŸĨ", - "version_check_implications": "į‰ˆæœŦæĒĸæŸĨ功čƒŊäģ°čŗ´čˆ‡ github.com įš„åŽšæœŸé€šč¨Š", + "version_check_implications": "į‰ˆæœŦæĒĸæŸĨ功čƒŊäģ°čŗ´čˆ‡ {server} įš„åŽšæœŸé€šč¨Š", "version_check_settings": "į‰ˆæœŦæĒĸæŸĨ", "version_check_settings_description": "å•Ÿį”¨ / åœį”¨æ–°į‰ˆæœŦ通įŸĨ", "video_conversion_job": "åŊąį‰‡čŊ‰įĸŧ", @@ -493,7 +493,7 @@ "album_selected": "åˇ˛é¸å–į›¸į°ŋ", "album_share_no_users": "įœ‹äž†æ‚¨čˆ‡æ‰€æœ‰äŊŋį”¨č€…å…ąäēĢäē†é€™æœŦᛏį°ŋīŧŒæˆ–æ˛’æœ‰å…ļäģ–äŊŋį”¨č€…å¯äž›åˆ†äēĢ。", "album_summary": "ᛏį°ŋ摘čρ", - "album_updated": "æ›´æ–°į›¸į°ŋ時", + "album_updated": "ᛏį°ŋåˇ˛æ›´æ–°", "album_updated_setting_description": "į•ļå…ąäēĢᛏį°ŋæœ‰æ–°é …į›Žæ™‚į”¨é›ģ子éƒĩäģļ通įŸĨ我", "album_upload_assets": "åžžæ‚¨įš„é›ģč…Ļä¸Šå‚ŗæĒ”æĄˆä¸Ļ加å…Ĩᛏį°ŋ", "album_user_left": "é›ĸ開 {album}", @@ -508,11 +508,11 @@ "album_viewer_page_share_add_users": "邀čĢ‹å…ļäģ–äēē", "album_with_link_access": "äģģäŊ•æ“æœ‰é€Ŗįĩįš„äēēįš†å¯æĒĸčĻ–æ­¤į›¸į°ŋä¸­įš„į›¸į‰‡čˆ‡äēēį‰Šã€‚", "albums": "ᛏį°ŋ", - "albums_count": "{count, plural, one {{count, number} 個ᛏį°ŋ} other {{count, number} 個ᛏį°ŋ}}", + "albums_count": "{count, plural, one {{count, number} æœŦᛏį°ŋ} other {{count, number} æœŦᛏį°ŋ}}", "albums_default_sort_order": "預荭ᛏį°ŋ排åē", "albums_default_sort_order_description": "åģēįĢ‹æ–°į›¸į°ŋ時čĻåˆå§‹åŒ–é …į›ŽæŽ’åēæ–šåŧã€‚", "albums_feature_description": "å¯å…ąäēĢįĩĻå…ļäģ–äŊŋį”¨č€…įš„é …į›Žé›†åˆã€‚", - "albums_on_device_count": "æ­¤čŖįŊŽæœ‰ ({count}) 個ᛏį°ŋ", + "albums_on_device_count": "æ­¤čŖįŊŽæœ‰ ({count}) æœŦᛏį°ŋ", "albums_selected": "{count, plural, one {åˇ˛é¸å– # æœŦᛏį°ŋ} other {åˇ˛é¸å– # æœŦᛏį°ŋ}}", "all": "全部", "all_albums": "æ‰€æœ‰į›¸į°ŋ", @@ -591,7 +591,7 @@ "assets_added_to_album_count": "厞將 {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}加å…Ĩ臺ᛏį°ŋ", "assets_added_to_albums_count": "厞將 {assetTotal, plural, other {# å€‹é …į›Ž}} 新åĸžč‡ŗ {albumTotal, plural, other {# æœŦᛏį°ŋ}}", "assets_cannot_be_added_to_album_count": "į„Ąæŗ•å°‡ {count, plural, one {é …į›Ž} other {é …į›Ž}} 加å…Ĩ臺ᛏį°ŋ", - "assets_cannot_be_added_to_albums": "į„Ąæŗ•å°‡ {count, plural, other {# å€‹é …į›Ž}} 加å…ĨäģģäŊ•ᛏį°ŋ", + "assets_cannot_be_added_to_albums": "į„Ąæŗ•å°‡ {count, plural, one {é …į›Ž} other {é …į›Ž}} 加å…ĨäģģäŊ•ᛏį°ŋ", "assets_count": "{count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}", "assets_deleted_permanently": "åˇ˛æ°¸äš…åˆĒ除 {count} å€‹é …į›Ž", "assets_deleted_permanently_from_server": "åˇ˛åžž Immich äŧ翜å™¨ä¸­æ°¸äš…į§ģ除 {count} å€‹é …į›Ž", @@ -608,21 +608,21 @@ "assets_trashed_count": "厞將 {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}į§ģč‡ŗåžƒåœžæĄļ", "assets_trashed_from_server": "åˇ˛åžž Immich äŧ翜å™¨å°‡ {count} å€‹é …į›Žį§ģč‡ŗåžƒåœžæĄļ", "assets_were_part_of_album_count": "{count, plural, one {čŠ˛é …į›Žåˇ˛} other {這äē›é …į›Žåˇ˛}}åœ¨į›¸į°ŋ中", - "assets_were_part_of_albums_count": "{count, plural, one {個} other {個}}é …į›Žåˇ˛čĸĢå„˛å­˜åœ¨į›¸į°ŋ中", + "assets_were_part_of_albums_count": "{count, plural, one {čŠ˛é …į›Žåˇ˛} other {這äē›é …į›Žåˇ˛}}存在æ–ŧᛏį°ŋ中", "authorized_devices": "åˇ˛æŽˆæŦŠčŖįŊŽ", "automatic_endpoint_switching_subtitle": "į•ļå¯į”¨æ™‚īŧŒé€éŽæŒ‡åŽšįš„ Wi-Fi 在æœŦæŠŸé€ŖįˇšīŧŒå…ļäģ–æƒ…æŗå‰‡äŊŋᔍæ›ŋäģŖé€Ŗįˇš", "automatic_endpoint_switching_title": "č‡Ē動 URL 切換", "autoplay_slideshow": "č‡Ē動播攞åšģį‡ˆį‰‡", "back": "上一頁", "back_close_deselect": "回上一頁、關閉ä¸Ļ取æļˆé¸å–", - "background_backup_running_error": "垌č‡ē備äģŊį›Žå‰æ­Ŗåœ¨åŸˇčĄŒīŧŒį„Ąæŗ•啟動手動備äģŊ", + "background_backup_running_error": "čƒŒæ™¯å‚™äģŊį›Žå‰æ­Ŗåœ¨åŸˇčĄŒīŧŒį„Ąæŗ•啟動手動備äģŊ", "background_location_permission": "čƒŒæ™¯å­˜å–äŊįŊŽæŦŠé™", "background_location_permission_content": "į‚ēäē†åœ¨čƒŒæ™¯åŸˇčĄŒæ™‚切換įļ˛čˇ¯īŧŒImmich åŋ…須始įĩ‚å…ˇæœ‰į˛žįĸēäŊįŊŽå­˜å–æŦŠé™īŧŒæ‰čƒŊčŽ€å– Wi-Fi įļ˛čˇ¯åį¨ą", "background_options": "čƒŒæ™¯é¸é …", "backup": "備äģŊ", "backup_album_selection_page_albums_device": "čŖįŊŽä¸Šįš„ᛏį°ŋīŧˆ{count}īŧ‰", "backup_album_selection_page_albums_tap": "éģžä¸€ä¸‹äģĨ選取īŧŒéģžå…Šä¸‹äģĨ排除", - "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_total_assets": "į¸Ŋä¸é‡č¤‡é …į›Žæ•¸", @@ -668,13 +668,13 @@ "backup_controller_page_remainder_sub": "é¸å–é …į›Žä¸­å°šæœĒ備äģŊįš„į›¸į‰‡čˆ‡åŊąį‰‡", "backup_controller_page_server_storage": "äŧ翜å™¨å„˛å­˜įŠē間", "backup_controller_page_start_backup": "開始備äģŊ", - "backup_controller_page_status_off": "前č‡ēč‡Ē動備äģŊåˇ˛é—œé–‰", - "backup_controller_page_status_on": "前č‡ēč‡Ē動備äģŊåˇ˛é–‹å•Ÿ", + "backup_controller_page_status_off": "前景č‡Ē動備äģŊåˇ˛é—œé–‰", + "backup_controller_page_status_on": "前景č‡Ē動備äģŊåˇ˛é–‹å•Ÿ", "backup_controller_page_storage_format": "{used} / {total} 厞äŊŋᔍ", "backup_controller_page_to_backup": "čρ備äģŊįš„į›¸į°ŋ", "backup_controller_page_total_sub": "åˇ˛é¸å–į›¸į°ŋä¸­įš„æ‰€æœ‰ä¸é‡č¤‡įš„į›¸į‰‡čˆ‡åŊąį‰‡", - "backup_controller_page_turn_off": "關閉前č‡ē備äģŊ", - "backup_controller_page_turn_on": "開啟前č‡ē備äģŊ", + "backup_controller_page_turn_off": "關閉前景備äģŊ", + "backup_controller_page_turn_on": "開啟前景備äģŊ", "backup_controller_page_uploading_file_info": "ä¸Šå‚ŗä¸­įš„æĒ”æĄˆčŗ‡č¨Š", "backup_err_only_album": "不čƒŊį§ģé™¤å”¯ä¸€įš„į›¸į°ŋ", "backup_error_sync_failed": "同æ­Ĩå¤ąæ•—īŧŒį„Ąæŗ•處ᐆ備äģŊ。", @@ -685,7 +685,7 @@ "backup_manual_title": "ä¸Šå‚ŗį‹€æ…‹", "backup_options": "備äģŊ選項", "backup_options_page_title": "備äģŊ選項", - "backup_setting_subtitle": "įŽĄį†čƒŒæ™¯čˆ‡å‰č‡ēä¸Šå‚ŗč¨­åŽš", + "backup_setting_subtitle": "įŽĄį†čƒŒæ™¯čˆ‡å‰æ™¯ä¸Šå‚ŗč¨­åŽš", "backup_settings_subtitle": "įŽĄį†ä¸Šå‚ŗč¨­åŽš", "backup_upload_details_page_more_details": "éģžæ“ŠæŸĨįœ‹æ›´å¤ščŠŗį´°čŗ‡č¨Š", "backward": "į”ąčˆŠč‡ŗæ–°", @@ -751,7 +751,7 @@ "change_your_password": "čŽŠæ›´æ‚¨įš„å¯†įĸŧ", "changed_visibility_successfully": "åˇ˛æˆåŠŸčŽŠæ›´å¯čĻ‹æ€§", "charging": "充é›ģ", - "charging_requirement_mobile_backup": "垌č‡ē備äģŊčĻæą‚čŖįŊŽæ­Ŗåœ¨å……é›ģ", + "charging_requirement_mobile_backup": "čƒŒæ™¯å‚™äģŊčĻæą‚čŖįŊŽæ­Ŗåœ¨å……é›ģ", "check_corrupt_asset_backup": "æĒĸæŸĨææ¯€įš„å‚™äģŊé …į›Ž", "check_corrupt_asset_backup_button": "åŸˇčĄŒæĒĸæŸĨ", "check_corrupt_asset_backup_description": "åƒ…åœ¨åˇ˛é€Ŗįˇšč‡ŗ Wi-Fi ä¸”æ‰€æœ‰é …į›Žåˇ˛åŽŒæˆå‚™äģŊåžŒåŸˇčĄŒæ­¤æĒĸæŸĨã€‚æ­¤į¨‹åŧå¯čƒŊ需čĻæ•¸åˆ†é˜ã€‚", @@ -761,11 +761,11 @@ "city": "城市", "cleanup_confirm_description": "Immich į™ŧįžæœ‰ {count} å€‹é …į›ŽīŧˆåģēįĢ‹æ–ŧ {date} 䚋前īŧ‰åˇ˛åމ免備äģŊ臺äŧ翜å™¨ã€‚是åĻčĻåžžæ­¤čŖįŊŽä¸­åˆĒ除æœŦ抟副æœŦīŧŸ", "cleanup_confirm_prompt_title": "åžžæ­¤čŖįŊŽåˆĒ除īŧŸ", - "cleanup_deleted_assets": "厞將{count}é …į›Žį§ģåˆ°čŖįŊŽįš„垃圞æĄļčŖĄ", + "cleanup_deleted_assets": "厞將 {count} å€‹é …į›Žį§ģåˆ°čŖįŊŽįš„垃圞æĄļčŖĄ", "cleanup_deleting": "æ­Ŗåœ¨į§ģ動到垃圞æĄļ...", - "cleanup_found_assets": "扞到{count}äģļåˇ˛ä¸Šå‚ŗįš„é …į›Ž", - "cleanup_found_assets_with_size": "扞到{count}äģļīŧŒį¸Ŋå…ą({size})åˇ˛ä¸Šå‚ŗįš„é …į›Ž", - "cleanup_icloud_shared_albums_excluded": "iCloudå…ąäēĢᛏį°ŋčĸ̿ޒ除æ–ŧ搜尋䚋外", + "cleanup_found_assets": "扞到 {count} äģļåˇ˛ä¸Šå‚ŗįš„é …į›Ž", + "cleanup_found_assets_with_size": "扞到 {count} äģļīŧŒį¸Ŋå…ą ({size}) åˇ˛ä¸Šå‚ŗįš„é …į›Ž", + "cleanup_icloud_shared_albums_excluded": "iCloud å…ąäēĢᛏį°ŋčĸ̿ޒ除æ–ŧ搜尋䚋外", "cleanup_no_assets_found": "扞不到įŦĻ合上čŋ°æĸäģļįš„é …į›Žã€‚é‡‹æ”žįŠē間功čƒŊ僅čƒŊį§ģ除厞備äģŊ臺äŧ翜å™¨įš„é …į›Ž", "cleanup_preview_title": "{count} 項需čρį§ģé™¤įš„é …į›Ž", "cleanup_step3_description": "掃描įŦĻ合æ—ĨæœŸčˆ‡å„˛å­˜č¨­åŽšįš„åˇ˛å‚™äģŊé …į›Žã€‚", @@ -782,8 +782,8 @@ "client_cert_import": "匯å…Ĩ", "client_cert_import_success_msg": "厞匝å…ĨᔍæˆļįĢ¯æ†‘č­‰", "client_cert_invalid_msg": "į„Ąæ•ˆįš„æ†‘č­‰æĒ”æĄˆæˆ–å¯†įĸŧ錯čǤ", - "client_cert_password_message": "čĢ‹čŧ¸å…Ĩæ­¤č­‰æ›¸įš„å¯†įĸŧ", - "client_cert_password_title": "č­‰æ›¸å¯†įĸŧ", + "client_cert_password_message": "čĢ‹čŧ¸å…Ĩæ­¤æ†‘č­‰įš„å¯†įĸŧ", + "client_cert_password_title": "æ†‘č­‰å¯†įĸŧ", "client_cert_remove_msg": "ᔍæˆļįĢ¯æ†‘č­‰åˇ˛į§ģ除", "client_cert_subtitle": "僅支援 PKCS12 (.p12, .pfx) æ ŧåŧã€‚æ†‘č­‰åŒ¯å…Ĩ與į§ģ除僅可在į™ģå…Ĩå‰é€˛čĄŒ", "client_cert_title": "SSL ᔍæˆļįĢ¯æ†‘č­‰ [å¯Ļ銗性]", @@ -794,7 +794,7 @@ "color": "顏色", "color_theme": "色åŊŠä¸ģ題", "command": "å‘Ŋäģ¤", - "command_palette_prompt": "åŋĢ速尋扞頁éĸīŧŒå‹•äŊœæˆ–č€…æŒ‡äģ¤", + "command_palette_prompt": "åŋĢ速搜尋頁éĸ、動äŊœæˆ–指äģ¤", "command_palette_to_close": "關閉", "command_palette_to_navigate": "čŧ¸å…Ĩ", "command_palette_to_select": "選擇", @@ -837,7 +837,7 @@ "copy_password": "複čŖŊ密įĸŧ", "copy_to_clipboard": "複čŖŊ到å‰Ēč˛ŧį°ŋ", "country": "國åŽļ", - "cover": "封éĸ", + "cover": "åĄĢæģŋ", "covers": "封éĸ", "create": "åģēįĢ‹", "create_album": "åģēį̋ᛏį°ŋ", @@ -849,9 +849,12 @@ "create_link_to_share": "åģēįĢ‹åˆ†äēĢ逪įĩ", "create_link_to_share_description": "æŒæœ‰é€Ŗįĩįš„äēēįš†å¯æĒĸčĻ–æ‰€é¸é …į›Ž", "create_new": "新åĸž", + "create_new_face": "åģēįĢ‹æ–°č‡‰å­”", "create_new_person": "åģēįĢ‹æ–°äēēį‰Š", "create_new_person_hint": "å°‡é¸å–įš„é …į›ŽæŒ‡æ´žįĩĻæ–°įš„äēēį‰Š", "create_new_user": "åģēįĢ‹æ–°äŊŋᔍ者", + "create_person": "åģēįĢ‹äēēį‰Š", + "create_person_subtitle": "į‚翉€é¸č‡‰å­”æ–°åĸžåå­—äģĨåģēįĢ‹å’Œæ¨™č¨˜æ–°äēēį‰Š", "create_shared_album_page_share_add_assets": "新åĸžé …į›Ž", "create_shared_album_page_share_select_photos": "é¸å–į›¸į‰‡", "create_shared_link": "åģēįĢ‹åˆ†äēĢ逪įĩ", @@ -866,13 +869,14 @@ "crop_aspect_ratio_fixed": "厞äŋŽåžŠ", "crop_aspect_ratio_free": "į„Ąé™åˆļ", "crop_aspect_ratio_original": "原æĒ”", + "crop_aspect_ratio_square": "æ–šåŊĸ", "curated_object_page_title": "äē‹į‰Š", "current_device": "į›Žå‰čŖįŊŽ", "current_pin_code": "į›Žå‰ PIN įĸŧ", "current_server_address": "į›Žå‰įš„äŧ翜å™¨äŊå€", "custom_date": "åĻ選æ—Ĩ期", "custom_locale": "č‡Ēč¨‚åœ°å€č¨­åŽš", - "custom_locale_description": "栚據čĒžč¨€čˆ‡åœ°å€æ ŧåŧåŒ–æ—ĨæœŸčˆ‡æ•¸å­—", + "custom_locale_description": "栚據選厚čĒžč¨€čˆ‡åœ°å€æ ŧåŧåŒ–æ—ĨæœŸã€æ™‚é–“čˆ‡æ•¸å­—", "custom_url": "č‡Ē訂 URL", "cutoff_date_description": "äŋį•™æœ€čŋ‘å¤šå°‘å¤Šįš„į›¸į‰‡â€Ļ", "cutoff_day": "{count, plural, one {夊} other {夊}}", @@ -880,7 +884,7 @@ "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "YYYY åš´ M 月 D æ—Ĩ (E)", "dark": "æˇąč‰˛", - "dark_theme": "åˆ‡æ›æˇąč‰˛ä¸ģ題", + "dark_theme": "åˆ‡æ›č‡ŗæˇąč‰˛ä¸ģ題", "date": "æ—Ĩ期", "date_after": "čĩˇå§‹æ—Ĩ期", "date_and_time": "æ—ĨæœŸčˆ‡æ™‚é–“", @@ -891,10 +895,8 @@ "day": "æ—Ĩ", "days": "æ—Ĩ", "deduplicate_all": "åˆĒé™¤æ‰€æœ‰é‡č¤‡é …į›Ž", - "deduplication_criteria_1": "åŊąåƒå¤§å°īŧˆäģĨäŊå…ƒįĩ„į‚ēå–ŽäŊīŧ‰", - "deduplication_criteria_2": "EXIF čŗ‡æ–™æ•¸é‡", - "deduplication_info": "é‡č¤‡čŗ‡æ–™åˆĒé™¤čŗ‡č¨Š", - "deduplication_info_description": "č‹Ĩčρč‡Ēå‹•é å…ˆé¸å–é …į›Žä¸Ļ扚æŦĄį§ģé™¤é‡č¤‡é …į›ŽīŧŒæˆ‘們會æĒĸæŸĨīŧš", + "default_locale": "預設čĒžč¨€", + "default_locale_description": "äŊŋᔍäŊ įš„į€čĻŊ器區域äģĨæ ŧåŧæ—Ĩ期和數字", "delete": "åˆĒ除", "delete_action_confirmation_message": "您įĸē厚čρåˆĒé™¤æ­¤é …į›Žå—ŽīŧŸæ­¤å‹•äŊœæœƒå°‡čŠ˛é …į›Žį§ģ臺äŧ翜å™¨įš„垃圞æĄļīŧŒä¸ĻčŠĸ問您是åĻčρ圍æœŦ抟同æ­ĨåˆĒ除", "delete_action_prompt": "{count} 個厞åˆĒ除", @@ -966,11 +968,11 @@ "download_waiting_to_retry": "į­‰åž…é‡čŠĻ", "downloading": "下čŧ‰ä¸­", "downloading_asset_filename": "æ­Ŗåœ¨ä¸‹čŧ‰é …į›Ž {filename}", - "downloading_from_icloud": "æ­ŖåžžiCloud下čŧ‰", + "downloading_from_icloud": "æ­Ŗåžž iCloud 下čŧ‰", "downloading_media": "æ­Ŗåœ¨ä¸‹čŧ‰åĒ’éĢ”", "drop_files_to_upload": "將æĒ”æĄˆæ‹–æ”žåˆ°äģģäŊ•äŊįŊŽäģĨä¸Šå‚ŗ", "duplicates": "é‡č¤‡é …į›Ž", - "duplicates_description": "逐一æĒĸæŸĨæ¯å€‹įž¤įĩ„īŧŒä¸Ļ標į¤ēå…ļ中是åĻæœ‰é‡č¤‡é …į›Ž", + "duplicates_description": "逐一æĒĸæŸĨæ¯å€‹įž¤įĩ„īŧŒä¸Ļ標į¤ēå…ļ中是åĻæœ‰é‡č¤‡é …į›Žã€‚", "duration": "éĄ¯į¤ēæ™‚é•ˇ", "edit": "ᎍčŧ¯", "edit_album": "ᎍčŧ¯į›¸į°ŋ", @@ -1007,10 +1009,12 @@ "editor_edits_applied_success": "åˇ˛æˆåŠŸåĨ—ᔍᎍčŧ¯", "editor_flip_horizontal": "æ°´åšŗįŋģčŊ‰", "editor_flip_vertical": "åž‚į›´įŋģčŊ‰", + "editor_handle_corner": "{corner, select, top_left {åˇĻ上角} top_right {åŗä¸Šč§’} bottom_left {åˇĻ下角} bottom_right {åŗä¸‹č§’} other {某個}}角čŊįš„æŽ§åˆļ手柄", + "editor_handle_edge": "{edge, select, top {頂部} bottom {åē•部} left {åˇĻ側} right {åŗå´} other {某個}} é‚ŠįˇŖįš„æŽ§åˆļ手柄", "editor_orientation": "斚向", "editor_reset_all_changes": "é‡č¨­čŽŠæ›´", - "editor_rotate_left": "逆時針旋čŊ‰90åēĻ", - "editor_rotate_right": "順時針旋čŊ‰90åēĻ", + "editor_rotate_left": "逆時針旋čŊ‰ 90 åēĻ", + "editor_rotate_right": "順時針旋čŊ‰ 90 åēĻ", "email": "é›ģ子éƒĩäģļ", "email_notifications": "é›ģ子éƒĩäģļ通įŸĨ", "empty_folder": "é€™å€‹čŗ‡æ–™å¤žæ˜¯įŠēįš„", @@ -1021,7 +1025,7 @@ "enable_biometric_auth_description": "čŧ¸å…Ĩæ‚¨įš„ PIN įĸŧäģĨå•Ÿį”¨į”Ÿį‰Ščž¨č­˜éŠ—č­‰", "enabled": "åˇ˛å•Ÿį”¨", "end_date": "įĩæŸæ—Ĩ期", - "enqueued": "åˇ˛æŽ’å…Ĩåēåˆ—", + "enqueued": "åˇ˛æŽ’å…ĨäŊ‡åˆ—", "enter_wifi_name": "čŧ¸å…Ĩ Wi-Fi åį¨ą", "enter_your_pin_code": "čŧ¸å…Ĩæ‚¨įš„ PIN įĸŧ", "enter_your_pin_code_subtitle": "čŧ¸å…Ĩæ‚¨įš„ PIN įĸŧäģĨå­˜å–ã€Œåˇ˛éŽ–åŽšã€čŗ‡æ–™å¤ž", @@ -1072,7 +1076,7 @@ "failed_to_update_notification_status": "į„Ąæŗ•æ›´æ–°é€šįŸĨį‹€æ…‹", "incorrect_email_or_password": "é›ģ子éƒĩäģ￈–密įĸŧ錯čǤ", "library_folder_already_exists": "此匯å…Ĩčˇ¯åž‘åˇ˛å­˜åœ¨ã€‚", - "page_not_found": "æœĒ扞到頁éĸ :/", + "page_not_found": "æœĒ扞到頁éĸ", "paths_validation_failed": "{paths, plural, one {# å€‹čˇ¯åž‘} other {# å€‹čˇ¯åž‘}} éŠ—č­‰å¤ąæ•—", "profile_picture_transparent_pixels": "個äēēčŗ‡æ–™åœ–į‰‡ä¸čƒŊ有透明į•Ģį´ ã€‚čĢ‹æ”žå¤§ä¸Ļ/或į§ģ動åŊąåƒã€‚", "quota_higher_than_disk_size": "æ‚¨č¨­åŽšįš„é…éĄå¤§æ–ŧ᪁įĸŸåŽšé‡", @@ -1343,11 +1347,11 @@ "ios_debug_info_processing_ran_at": "æ–ŧ {dateTime} åŸˇčĄŒč™•į†", "items_count": "{count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}", "jobs": "äģģ務", - "json_editor": "JSONᎍčŧ¯å™¨", - "json_error": "JSON錯čǤ", + "json_editor": "JSON ᎍčŧ¯å™¨", + "json_error": "JSON 錯čǤ", "keep": "äŋį•™", "keep_albums": "äŋį•™į›¸į°ŋ", - "keep_albums_count": "äŋį•™{count} {count, plural, one {個ᛏį°ŋ} other {個ᛏį°ŋ}}", + "keep_albums_count": "äŋį•™{count} {count, plural, one {æœŦᛏį°ŋ} other {æœŦᛏį°ŋ}}", "keep_all": "全部äŋį•™", "keep_description": "é¸æ“‡åŸˇčĄŒé‡‹æ”žįŠē間時čρäŋį•™åœ¨čŖįŊŽä¸Šįš„é …į›Žã€‚", "keep_favorites": "äŋį•™æœ€æ„›įš„ᛏቇ", @@ -1355,7 +1359,7 @@ "keep_on_device_hint": "選擇äŋį•™åœ¨čŖįŊŽä¸Šįš„ᛏቇ", "keep_this_delete_others": "äŋį•™é€™å€‹īŧŒåˆĒ除å…ļäģ–", "keeping": "äŋį•™:{items}", - "kept_this_deleted_others": "äŋį•™é€™å€‹é …į›Žä¸ĻåˆĒ除{count, plural, one {# asset} other {# assets}}", + "kept_this_deleted_others": "äŋį•™é€™å€‹é …į›Žä¸ĻåˆĒ除{count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}", "keyboard_shortcuts": "éĩᛤåŋĢæˇéĩ", "language": "čĒžč¨€", "language_no_results_subtitle": "čŠĻ著čĒŋæ•´æ‚¨įš„æœå°‹čŠžåŊ™", @@ -1385,9 +1389,11 @@ "library_page_sort_title": "ᛏį°ŋæ¨™éĄŒ", "licenses": "授æŦŠ", "light": "æˇē色", + "light_theme": "åˆ‡æ›č‡ŗæˇē色ä¸ģ題", "like": "å–œæ­Ą", "like_deleted": "åˇ˛å–æļˆå–œæ­Ą", "link_motion_video": "逪įĩå‹•æ…‹åŊąį‰‡", + "link_to_docs": "čĢ‹åƒé–ą čĒĒæ˜Žæ–‡äģļ äģĨį˛å–æ›´å¤ščŗ‡č¨Šã€‚", "link_to_oauth": "逪įĩ OAuth", "linked_oauth_account": "厞逪įĩ OAuth å¸ŗč™Ÿ", "list": "清喎", @@ -1396,7 +1402,7 @@ "local": "æœŦ抟", "local_asset_cast_failed": "į„Ąæŗ•æŠ•æ”žæœĒä¸Šå‚ŗč‡ŗäŧ翜å™¨įš„é …į›Ž", "local_assets": "æœŦæŠŸé …į›Ž", - "local_id": "æœŦ地ID", + "local_id": "æœŦ地 ID", "local_media_summary": "æœŦ抟åĒ’éĢ”æ‘˜čρ", "local_network": "æœŦ抟įļ˛čˇ¯", "local_network_sheet_info": "į•ļäŊŋį”¨æŒ‡åŽšįš„ Wi-Fi įļ˛čˇ¯æ™‚īŧŒæ‡‰į”¨į¨‹åŧå°‡é€éŽæ­¤įļ˛å€é€Ŗįˇšč‡ŗäŧ翜å™¨", @@ -1485,14 +1491,14 @@ "manage_your_devices": "įŽĄį†åˇ˛į™ģå…Ĩįš„čŖįŊŽ", "manage_your_oauth_connection": "įŽĄį†æ‚¨įš„ OAuth 逪įĩ", "map": "地圖", - "map_assets_in_bounds": "{count, plural, one {# åŧĩᛏቇ} other {# åŧĩᛏቇ}}", + "map_assets_in_bounds": "{count, plural, =0 {æ­¤å€åŸŸæ˛’æœ‰į›¸į‰‡} one {# åŧĩᛏቇ} other {# åŧĩᛏቇ}}", "map_cannot_get_user_location": "į„Ąæŗ•å–åž—äŊŋᔍ者äŊįŊŽ", "map_location_dialog_yes": "įĸē厚", "map_location_picker_page_use_location": "äŊŋį”¨æ­¤äŊįŊŽ", "map_location_service_disabled_content": "需čĻå•Ÿį”¨åŽšäŊæœå‹™æ‰čƒŊéĄ¯į¤ēæ‚¨į›Žå‰äŊįŊŽį›¸é—œįš„é …į›Žã€‚čĻįžåœ¨å•Ÿį”¨å—ŽīŧŸ", "map_location_service_disabled_title": "厚äŊæœå‹™åˇ˛åœį”¨", - "map_marker_for_images": "在 {city}、{country} 拍攝åŊąåƒįš„地圖į¤ē記", - "map_marker_with_image": "å¸ļ有åŊąåƒįš„地圖į¤ē記", + "map_marker_for_images": "在 {city}、{country} 拍攝åŊąåƒįš„åœ°åœ–æ¨™č¨˜", + "map_marker_with_image": "å¸ļ有åŊąåƒįš„åœ°åœ–æ¨™č¨˜", "map_no_location_permission_content": "需čρäŊįŊŽæŦŠé™æ‰čƒŊéĄ¯į¤ēčˆ‡æ‚¨į›Žå‰äŊįŊŽį›¸é—œįš„é …į›Žã€‚čĻįžåœ¨å°ąæŽˆäēˆäŊįŊŽæŦŠé™å—ŽīŧŸ", "map_no_location_permission_title": "æ˛’æœ‰äŊįŊŽæŦŠé™", "map_settings": "åœ°åœ–č¨­åŽš", @@ -1550,7 +1556,7 @@ "move_to_locked_folder_confirmation": "這äē›į›¸į‰‡čˆ‡åŊąį‰‡å°‡åžžæ‰€æœ‰į›¸į°ŋ中į§ģ除īŧŒä¸”僅čƒŊåžžã€Œåˇ˛éŽ–åŽšã€čŗ‡æ–™å¤žä¸­æĒĸčĻ–", "move_up": "向上į§ģ動", "moved_to_archive": "厞封存 {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}", - "moved_to_library": "厞į§ģ動 {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": "å”¯čŽ€é …į›Žįš„äŊįŊŽčŗ‡č¨Šį„Ąæŗ•ᎍčŧ¯īŧŒåˇ˛į•Ĩ過", @@ -1559,12 +1565,12 @@ "name": "åį¨ą", "name_or_nickname": "åį¨ąæˆ–æšąį¨ą", "name_required": "åį¨ąæ˜¯åŋ…åĄĢ項", - "navigate": "導čˆĒ", + "navigate": "導čĻŊ", "navigate_to_time": "莺čŊ‰č‡ŗæŒ‡åŽšæ™‚é–“", "network_requirement_photos_upload": "äŊŋį”¨čĄŒå‹•įļ˛čˇ¯æĩé‡å‚™äģŊᛏቇ", "network_requirement_videos_upload": "äŊŋį”¨čĄŒå‹•įļ˛čˇ¯æĩé‡å‚™äģŊåŊąį‰‡", "network_requirements": "įļ˛čˇ¯čĻæą‚", - "network_requirements_updated": "įļ˛čˇ¯éœ€æą‚åˇ˛čŽŠæ›´īŧŒæ­Ŗåœ¨é‡č¨­å‚™äģŊåēåˆ—", + "network_requirements_updated": "įļ˛čˇ¯éœ€æą‚åˇ˛čŽŠæ›´īŧŒæ­Ŗåœ¨é‡č¨­å‚™äģŊäŊ‡åˆ—", "networking_settings": "įļ˛čˇ¯", "networking_subtitle": "įŽĄį†äŧ翜å™¨į̝éģžč¨­åޚ", "never": "æ°¸ä¸å¤ąæ•ˆ", @@ -1588,7 +1594,7 @@ "no_albums_message": "åģēį̋ᛏį°ŋäž†æ•´į†į›¸į‰‡å’ŒåŊąį‰‡", "no_albums_with_name_yet": "įœ‹äž†é‚„æ˛’æœ‰é€™å€‹åå­—įš„į›¸į°ŋ。", "no_albums_yet": "įœ‹äž†æ‚¨é‚„æ˛’æœ‰äģģäŊ•ᛏį°ŋ。", - "no_archived_assets_message": "å°‡į›¸į‰‡čˆ‡åŊąį‰‡å°å­˜åžŒīŧŒå°ąä¸æœƒéĄ¯į¤ēåœ¨ã€Œį›¸į‰‡ã€čĻ–åœ–ä¸­", + "no_archived_assets_message": "å°‡į›¸į‰‡čˆ‡åŊąį‰‡å°å­˜åžŒīŧŒå°ąä¸æœƒéĄ¯į¤ēåœ¨ã€Œį›¸į‰‡ã€é éĸ中", "no_assets_message": "æŒ‰é€™čŖĄä¸Šå‚ŗæ‚¨įš„įŦŦ一åŧĩᛏቇ", "no_assets_to_show": "į„Ąé …į›Žåą•į¤ē", "no_cast_devices_found": "扞不到 Google Cast čŖįŊŽ", @@ -1649,6 +1655,7 @@ "only_favorites": "åƒ…éĄ¯į¤ēåˇąæ”ļ藏", "open": "開啟", "open_calendar": "打開æ—Ĩ曆", + "open_in_browser": "åœ¨į€čĻŊ器中開啟", "open_in_map_view": "開啟地圖æĒĸčĻ–", "open_in_openstreetmap": "ᔍ OpenStreetMap 開啟", "open_the_search_filters": "é–‹å•Ÿæœå°‹į¯Šé¸å™¨", @@ -1703,7 +1710,7 @@ "permanent_deletion_warning_setting_description": "在永䚅åˆĒ除æĒ”æĄˆæ™‚éĄ¯į¤ēč­Ļ告", "permanently_delete": "永䚅åˆĒ除", "permanently_delete_assets_count": "永䚅åˆĒ除 {count, plural, one {æĒ”æĄˆ} other {æĒ”æĄˆ}}", - "permanently_delete_assets_prompt": "įĸē厚čĻæ°¸äš…åˆĒ除 {count, plural, other {這 # 個æĒ”æĄˆīŧŸ}}é€™æ¨Ŗ{count, plural, one {厃} other {厃們}}䚟會垞č‡Ēåˇąæ‰€åœ¨įš„į›¸į°ŋ中æļˆå¤ąã€‚", + "permanently_delete_assets_prompt": "įĸē厚čĻæ°¸äš…åˆĒ除 {count, plural, one {這個æĒ”æĄˆīŧŸ} other {這 # 個æĒ”æĄˆīŧŸ}}é€™æ¨Ŗ{count, plural, one {厃} other {厃們}}䚟會垞č‡Ēåˇąæ‰€åœ¨įš„į›¸į°ŋ中æļˆå¤ąã€‚", "permanently_deleted_asset": "永䚅åˆĒé™¤įš„æĒ”æĄˆ", "permanently_deleted_assets_count": "永䚅åˆĒé™¤įš„ {count, plural, one {# 個æĒ”æĄˆ} other {# 個æĒ”æĄˆ}}", "permission": "æŦŠé™", @@ -1766,7 +1773,7 @@ "profile_drawer_app_logs": "į´€éŒ„", "profile_drawer_client_server_up_to_date": "ᔍæˆļįĢ¯čˆ‡äŧ翜å™¨į‰ˆæœŦįš†į‚ē最新", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "å”¯čŽ€æ¨Ąåŧåˇ˛å•Ÿį”¨ã€‚é•ˇæŒ‰äŊŋᔍ者個äēē圖į¤ēåŗå¯é€€å‡ē。", + "profile_drawer_readonly_mode": "å”¯čŽ€æ¨Ąåŧåˇ˛å•Ÿį”¨ã€‚é•ˇæŒ‰äŊŋᔍ者個äēē圖į¤ēåŗå¯é—œé–‰ã€‚", "profile_image_of_user": "{user} įš„å€‹äēēčŗ‡æ–™åœ–į‰‡", "profile_picture_set": "åˇ˛č¨­åŽšå€‹äēēčŗ‡æ–™åœ–į‰‡ã€‚", "public_album": "å…Ŧ開ᛏį°ŋ", @@ -1808,7 +1815,7 @@ "rate_asset": "é …į›ŽčŠ•åˆ†", "rating": "čŠ•æ˜Ÿ", "rating_clear": "æ¸…é™¤čŠ•į­‰", - "rating_count": "{count, plural, =0 {Unrated} other {# 星}}", + "rating_count": "{count, plural, =0 {æœĒčŠ•åˆ†} one {# 星} other {# 星}}", "rating_description": "åœ¨čŗ‡č¨Šéĸæŋä¸­éĄ¯į¤ē EXIF čŠ•į­‰", "reaction_options": "反應選項", "read_changelog": "閱čĻŊæ›´æ–°į´€éŒ„", @@ -1828,15 +1835,15 @@ "recently_taken_page_title": "最čŋ‘拍攝", "refresh": "é‡æ–°æ•´į†", "refresh_encoded_videos": "é‡æ–°æ•´į†åˇ˛įˇ¨įĸŧįš„åŊąį‰‡", - "refresh_faces": "重整éĸéƒ¨čŗ‡æ–™", + "refresh_faces": "é‡æ–°æ•´į†č‡‰å­”čŗ‡æ–™", "refresh_metadata": "é‡æ–°æ•´į†ä¸­įšŧčŗ‡æ–™", "refresh_thumbnails": "é‡æ–°æ•´į†į¸Žåœ–", "refreshed": "é‡æ–°æ•´į†åŽŒį•ĸ", "refreshes_every_file": "é‡æ–°čŽ€å–æ‰€æœ‰įžæœ‰čˆ‡æ–°åĸžæĒ”æĄˆ", "refreshing_encoded_video": "æ­Ŗåœ¨é‡æ–°æ•´į†åˇ˛įˇ¨įĸŧįš„åŊąį‰‡", - "refreshing_faces": "重整éĸéƒ¨čŗ‡æ–™ä¸­", + "refreshing_faces": "æ­Ŗåœ¨é‡æ–°æ•´į†č‡‰å­”čŗ‡æ–™", "refreshing_metadata": "æ­Ŗåœ¨é‡æ–°æ•´į†ä¸­įšŧčŗ‡æ–™", - "regenerating_thumbnails": "重新į”ĸį”Ÿį¸Žåœ–ä¸­", + "regenerating_thumbnails": "æ­Ŗåœ¨é‡æ–°į”ĸį”Ÿį¸Žåœ–", "remote": "遠į̝", "remote_assets": "遠įĢ¯é …į›Ž", "remote_media_summary": "遠į̝åĒ’éĢ”æ‘˜čρ", @@ -1865,8 +1872,8 @@ "removed_memory": "厞į§ģ除記æ†ļ", "removed_photo_from_memory": "åˇ˛åžžč¨˜æ†ļ中į§ģ除ᛏቇ", "removed_tagged_assets": "厞į§ģ除 {count, plural, one {# 個æĒ”æĄˆ} other {# 個æĒ”æĄˆ}}įš„æ¨™įą¤", - "rename": "攚名", - "repair": "įŗžæ­Ŗ", + "rename": "重新å‘Ŋ名", + "repair": "äŋŽåžŠ", "repair_no_results_message": "æœĒčĸĢčŋŊčš¤åŠéēå¤ąįš„æĒ”æĄˆæœƒéĄ¯į¤ēåœ¨é€™čŖĄ", "replace_with_upload": "į”¨ä¸Šå‚ŗįš„æĒ”æĄˆå–äģŖ", "repository": "å„˛å­˜åēĢ", @@ -1881,10 +1888,10 @@ "reset_pin_code_success": "PIN įĸŧåˇ˛æˆåŠŸé‡č¨­", "reset_pin_code_with_password": "您可隨時äŊŋį”¨æ‚¨įš„å¯†įĸŧ來重設 PIN įĸŧ", "reset_sqlite": "重設 SQLite čŗ‡æ–™åēĢ", - "reset_sqlite_clear_app_data": "清除數據", - "reset_sqlite_confirmation": "įĸē厚čĻé‡č¨­æ‰€æœ‰æ•¸æ“šå—ŽīŧŸäŊ įš„æ‰€æœ‰č¨­įŊŽå°‡čĸĢ重設īŧŒä¸”äŊ æœƒčĸĢį™ģå‡ē。", - "reset_sqlite_confirmation_note": "æŗ¨æ„īŧšäŊ éœ€čĻåœ¨æ¸…é™¤æ•¸æ“šåžŒé‡æ–°é–‹å•Ÿæ‡‰į”¨ã€‚", - "reset_sqlite_done": "æ•¸æ“šåˇ˛æ¸…é™¤ã€‚čĢ‹é‡å•ŸImmich及重新į™ģ錄。", + "reset_sqlite_clear_app_data": "æ¸…é™¤čŗ‡æ–™", + "reset_sqlite_confirmation": "įĸē厚čĻé‡č¨­æ‰€æœ‰čŗ‡æ–™å—ŽīŧŸäŊ įš„æ‰€æœ‰č¨­åŽšå°‡čĸĢ重設īŧŒä¸”äŊ æœƒčĸĢį™ģå‡ē。", + "reset_sqlite_confirmation_note": "æŗ¨æ„īŧšäŊ éœ€čĻåœ¨æ¸…é™¤čŗ‡æ–™åžŒé‡æ–°é–‹å•Ÿ App。", + "reset_sqlite_done": "čŗ‡æ–™åˇ˛æ¸…é™¤ã€‚čĢ‹é‡å•Ÿ Immich 及重新į™ģå…Ĩ。", "reset_sqlite_success": "åˇ˛æˆåŠŸé‡č¨­ SQLite čŗ‡æ–™åēĢ", "reset_to_default": "重設į‚ē預設å€ŧ", "resolution": "č§ŖæžåēĻ", @@ -1906,13 +1913,13 @@ "running": "åŸˇčĄŒä¸­", "save": "å„˛å­˜", "save_to_gallery": "å„˛å­˜åˆ°į›¸į°ŋ", - "saved": "厞äŋå­˜", + "saved": "åˇ˛å„˛å­˜", "saved_api_key": "åˇ˛å„˛å­˜ API 金鑰", "saved_profile": "åˇ˛å„˛å­˜å€‹äēēčŗ‡æ–™", "saved_settings": "åˇ˛å„˛å­˜č¨­åŽš", "say_something": "čĒĒčĒĒæ‚¨įš„æƒŗæŗ•吧", "scaffold_body_error_occurred": "į™ŧį”ŸéŒ¯čǤ", - "scaffold_body_error_unrecoverable": "į™ŧį”Ÿį„Ąæŗ•æĸåžŠįš„éŒ¯čĒ¤ã€‚čĢ‹åœ¨ Discord 或 Github 上分äēĢ錯čǤäŋĄæ¯åŠå †į–ŠčŋŊ蚤īŧŒäģĨäžŋ我們提䞛協劊。在čĸĢåģēč­°įš„æƒ…æŗä¸‹äŊ å¯äģĨ在下斚嘗čŠĻæ¸…é™¤į¨‹åŧæ•¸æ“šã€‚", + "scaffold_body_error_unrecoverable": "į™ŧį”Ÿį„Ąæŗ•æĸåžŠįš„éŒ¯čĒ¤ã€‚čĢ‹åœ¨ Discord 或 Github 上分äēĢ錯čĒ¤čŗ‡č¨ŠåŠå †į–ŠčŋŊ蚤īŧŒäģĨäžŋ我們提䞛協劊。在čĸĢåģēč­°įš„æƒ…æŗä¸‹äŊ å¯äģĨ在下斚嘗čŠĻæ¸…é™¤į¨‹åŧčŗ‡æ–™ã€‚", "scan": "掃描", "scan_all_libraries": "æŽƒææ‰€æœ‰į›¸į°ŋ", "scan_library": "掃描", @@ -1926,9 +1933,9 @@ "search_by_description_example": "åœ¨æ˛™åŖŠįš„åĨ行之æ—Ĩ", "search_by_filename": "䞝æĒ”名或副æĒ”名搜尋", "search_by_filename_example": "åĻ‚ IMG_1234.JPG 或 PNG", - "search_by_ocr": "透過OCR搜尋", + "search_by_ocr": "透過 OCR 搜尋", "search_by_ocr_example": "æ‹ŋéĩ", - "search_camera_lens_model": "蒐į´ĸéĄé ­åž‹č™Ÿ...", + "search_camera_lens_model": "æœå°‹éĄé ­åž‹č™Ÿ...", "search_camera_make": "æœå°‹į›¸æŠŸčŖŊ造商â€Ļ", "search_camera_model": "æœå°‹į›¸æŠŸåž‹č™Ÿâ€Ļ", "search_city": "搜尋城市â€Ļ", @@ -1945,7 +1952,7 @@ "search_filter_location_title": "選擇äŊįŊŽ", "search_filter_media_type": "åĒ’éĢ”éĄžåž‹", "search_filter_media_type_title": "選擇åĒ’éĢ”éĄžåž‹", - "search_filter_ocr": "透過OCR搜尋", + "search_filter_ocr": "透過 OCR 搜尋", "search_filter_people_title": "選擇äēēį‰Š", "search_filter_star_rating": "čŠ•åˆ†", "search_filter_tags_title": "é¸æ“‡æ¨™įą¤", @@ -1974,7 +1981,7 @@ "search_settings": "æœå°‹č¨­åŽš", "search_state": "搜尋地區â€Ļ", "search_suggestion_list_smart_search_hint_1": "æ™ē慧搜尋功čƒŊé č¨­åˇ˛å•Ÿį”¨īŧŒåĻ‚čĻæœå°‹ä¸­įšŧčŗ‡æ–™īŧŒčĢ‹äŊŋᔍčĒžæŗ• ", - "search_suggestion_list_smart_search_hint_2": "m:æ‚¨įš„æœå°‹é—œéĩ詞", + "search_suggestion_list_smart_search_hint_2": "m:æ‚¨įš„æœå°‹é—œéĩ字", "search_tags": "æœå°‹æ¨™įą¤...", "search_timezone": "搜尋時區â€Ļ", "search_type": "æœå°‹éĄžåž‹", @@ -2028,7 +2035,7 @@ "set_profile_picture": "č¨­åŽšå€‹äēēčŗ‡æ–™åœ–į‰‡", "set_slideshow_to_fullscreen": "äģĨ全čžĸ嚕攞映åšģį‡ˆį‰‡", "set_stack_primary_asset": "č¨­åŽšå †į–Šįš„éĻ–čĻé …į›Ž", - "setting_image_navigation_enable_subtitle": "開啟垌äģĨ觸įĸ°åąåš•åˇĻ/åŗé‚ŠįˇŖå€åŸŸįš„æ–šåŧåˆ‡æ›ä¸Š/ä¸‹åœ–į‰‡ã€‚", + "setting_image_navigation_enable_subtitle": "開啟垌äģĨ觸įĸ°čžĸåš•åˇĻ/åŗé‚ŠįˇŖå€åŸŸįš„æ–šåŧåˆ‡æ›ä¸Š/ä¸‹åœ–į‰‡ã€‚", "setting_image_navigation_enable_title": "éģžæ“Šåˆ‡æ›", "setting_image_navigation_title": "åœ–į‰‡å°Žåŧ•", "setting_image_viewer_help": "čŠŗį´°čŗ‡č¨ŠæĒĸčĻ–å™¨æœƒäžåēčŧ‰å…Ĩå°åž‹į¸Žåœ–ã€ä¸­į­‰å°ē寸預čĻŊ圖īŧˆč‹Ĩå•Ÿį”¨īŧ‰īŧŒæœ€åžŒčŧ‰å…ĨåŽŸå§‹į›¸į‰‡ã€‚", @@ -2128,7 +2135,7 @@ "show_all_people": "éĄ¯į¤ē所有äēēį‰Š", "show_and_hide_people": "éĄ¯į¤ē與隱藏äēēį‰Š", "show_file_location": "éĄ¯į¤ēæĒ”æĄˆäŊįŊŽ", - "show_gallery": "éĄ¯į¤ēį•ĢåģŠ", + "show_gallery": "éĄ¯į¤ēåĒ’éĢ”åēĢ", "show_hidden_people": "éĄ¯į¤ēéšąč—įš„äēēį‰Š", "show_in_timeline": "在時間čģ¸ä¸­éĄ¯į¤ē", "show_in_timeline_setting_description": "åœ¨æ‚¨įš„æ™‚é–“čģ¸ä¸­éĄ¯į¤ē這äŊäŊŋį”¨č€…įš„į›¸į‰‡å’ŒåŊąį‰‡", @@ -2145,7 +2152,7 @@ "show_supporter_badge": "æ”¯æŒč€…åžŊįĢ ", "show_supporter_badge_description": "éĄ¯į¤ēæ”¯æŒč€…åžŊįĢ ", "show_text_recognition": "éĄ¯į¤ēæ–‡å­—čž¨č­˜", - "show_text_search_menu": "éĄ¯į¤ēæ–‡å­—č’į´ĸ選喎", + "show_text_search_menu": "éĄ¯į¤ē文字搜尋選喎", "shuffle": "隨抟排åē", "sidebar": "側邊æŦ„", "sidebar_display_description": "在側邊æŦ„ä¸­éĄ¯į¤ē逪įĩ", @@ -2210,6 +2217,7 @@ "tag": "æ¨™įą¤", "tag_assets": "æ¨™č¨˜æĒ”æĄˆ", "tag_created": "厞åģēįĢ‹æ¨™įą¤īŧš{tag}", + "tag_face": "æ¨™č¨˜č‡‰å­”", "tag_feature_description": "äģĨ邏čŧ¯æ¨™č¨˜čĻæ—¨åˆ†éĄžį€čĻŊį›¸į‰‡å’ŒåŊąį‰‡", "tag_not_found_question": "æ‰žä¸åˆ°æ¨™įą¤īŧŸåģēįĢ‹æ–°æ¨™įą¤ã€‚", "tag_people": "æ¨™įą¤äēēį‰Š", @@ -2260,11 +2268,11 @@ "trash_all": "全部丟掉", "trash_count": "丟掉 {count, number} 個æĒ”æĄˆ", "trash_delete_asset": "將æĒ”æĄˆä¸Ÿé€˛åžƒåœžæĄļ / åˆĒ除", - "trash_emptied": "åˇ˛æ¸…įŠē回æ”ļæĄļ", + "trash_emptied": "åˇ˛æ¸…įŠē垃圞æĄļ", "trash_no_results_message": "垃圞æĄļä¸­įš„į›¸į‰‡å’ŒåŊąį‰‡å°‡éĄ¯į¤ēåœ¨é€™čŖĄã€‚", "trash_page_delete_all": "åˆĒ除全部", - "trash_page_empty_trash_dialog_content": "是åĻ清įŠē回æ”ļæĄļīŧŸé€™äē›é …į›Žå°‡čĸĢåžž Immich 中永䚅åˆĒ除", - "trash_page_info": "回æ”ļæĄļä¸­é …į›Žå°‡åœ¨ {days} 夊垌永䚅åˆĒ除", + "trash_page_empty_trash_dialog_content": "是åĻ清įŠē垃圞æĄļīŧŸé€™äē›é …į›Žå°‡čĸĢåžž Immich 中永䚅åˆĒ除", + "trash_page_info": "垃圞æĄļä¸­é …į›Žå°‡åœ¨ {days} 夊垌永䚅åˆĒ除", "trash_page_no_assets": "æšĢį„Ąåˇ˛åˆĒé™¤é …į›Ž", "trash_page_restore_all": "全部還原", "trash_page_select_assets_btn": "é¸æ“‡é …į›Ž", @@ -2277,7 +2285,7 @@ "trigger_person_recognized": "åˇ˛čž¨č­˜äēēį‰Š", "trigger_person_recognized_description": "åĩæ¸Ŧ到äēēį‰Šæ™‚č§¸į™ŧ", "trigger_type": "觸į™ŧéĄžåž‹", - "troubleshoot": "ᖑ雪觪᭔", + "troubleshoot": "į–‘é›ŖæŽ’č§Ŗ", "type": "éĄžåž‹", "unable_to_change_pin_code": "į„Ąæŗ•čŽŠæ›´ PIN įĸŧ", "unable_to_check_version": "į„Ąæŗ•æĒĸæŸĨæ‡‰į”¨į¨‹åŧæˆ–äŧ翜å™¨į‰ˆæœŦ", @@ -2309,7 +2317,7 @@ "unstack_action_prompt": "{count} 個取æļˆå †į–Š", "unstacked_assets_count": "åˇ˛č§Ŗé™¤å †į–Š {count, plural, other {# 個æĒ”æĄˆ}}", "unsupported_field_type": "ä¸æ”¯æ´įš„æŦ„äŊéĄžåž‹", - "unsupported_file_type": "不支持 {type} éĄžåž‹įš„æĒ”æĄˆīŧŒį„Ąæŗ•ä¸Šå‚ŗ {file} 文äģļ。", + "unsupported_file_type": "不支援 {type} éĄžåž‹įš„æĒ”æĄˆīŧŒį„Ąæŗ•ä¸Šå‚ŗ {file} æĒ”æĄˆã€‚", "untagged": "į„Ąæ¨™įą¤", "untitled_workflow": "æœĒå‘Ŋ名åˇĨäŊœæĩį¨‹", "up_next": "下一個", @@ -2337,6 +2345,7 @@ "usage": "į”¨é‡", "use_biometric": "äŊŋį”¨į”Ÿį‰Ščž¨č­˜", "use_browser_locale": "äŊŋį”¨į€čĻŊ器čĒžč¨€", + "use_browser_locale_description": "栚據äŊ į€čĻŊå™¨įš„čĒžč¨€å’Œåœ°å€č¨­åŽšäž†æ›´æ”šæ—Ĩ期īŧŒæ™‚é–“å’Œæ•¸å­—įš„æ ŧåŧ", "use_current_connection": "äŊŋį”¨į›Žå‰įš„é€Ŗįˇš", "use_custom_date_range": "æ”šį”¨č‡Ē訂æ—ĨæœŸį¯„åœ", "user": "äŊŋᔍ者", @@ -2366,7 +2375,7 @@ "version_history": "į‰ˆæœŦį´€éŒ„", "version_history_item": "{date} åŽ‰čŖäē† {version}", "video": "åŊąį‰‡", - "video_hover_setting": "éŠæ¨™åœį•™æ™‚æ’­æ”žåŊąį‰‡į¸Žåœ–", + "video_hover_setting": "æ¸¸æ¨™åœį•™æ™‚æ’­æ”žåŊąį‰‡į¸Žåœ–", "video_hover_setting_description": "į•ļæģ‘éŧ åœåœ¨é …į›Žä¸Šæ™‚æ’­æ”žåŊąį‰‡į¸Žåœ–ã€‚åŗäŊŋåœį”¨æ­¤åŠŸčƒŊīŧŒäģå¯é€éŽå°‡æģ‘éŧ åœåœ¨æ’­æ”žåœ–į¤ē上䞆開始播攞。", "videos": "åŊąį‰‡", "videos_count": "{count, plural, other {# 部åŊąį‰‡}}", @@ -2390,6 +2399,7 @@ "viewer_remove_from_stack": "åžžå †į–Šä¸­į§ģ除", "viewer_stack_use_as_main_asset": "äŊœį‚ēä¸ģé …į›ŽäŊŋᔍ", "viewer_unstack": "取æļˆå †į–Š", + "visibility": "可čĻ–æ€§", "visibility_changed": "åˇ˛čŽŠæ›´ {count, plural, other {# äŊäēēį‰Š}}įš„å¯čĻ‹æ€§", "visual": "čĻ–čĻēįš„", "visual_builder": "čĻ–čĻēæ§‹åģē器", diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 89480a8cb8..8126ff0859 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,8 +1,8 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:aa23850b91cb4c7faedac8ca9aa74ddc6eb03529a519145a589a7f35df4c5927 AS builder-cpu +FROM python:3.11-bookworm@sha256:970c99f886b839fc8829289040c1845dadaf2cae46b37acc7710333158ec29b4 AS builder-cpu -FROM python:3.13-slim-trixie@sha256:3de9a8d7aedbb7984dc18f2dff178a7850f16c1ae7c34ba9d7ecc23d0755e35f AS builder-openvino +FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec5fb2f8f6403244621664 AS builder-openvino FROM builder-cpu AS builder-cuda @@ -39,12 +39,12 @@ RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy -FROM python:3.11-slim-bookworm@sha256:04cd27899595a99dfe77709d96f08876bf2ee99139ee2f0fe9ac948005034e5b AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:9c6f90801e6b68e772b7c0ca74260cbf7af9f320acec894e26fccdaccfbe3b47 AS prod-cpu ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \ MACHINE_LEARNING_MODEL_ARENA=false -FROM python:3.13-slim-trixie@sha256:3de9a8d7aedbb7984dc18f2dff178a7850f16c1ae7c34ba9d7ecc23d0755e35f AS prod-openvino +FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec5fb2f8f6403244621664 AS prod-openvino RUN apt-get update && \ apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index ffbd9f720a..640996f54a 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "immich-ml" -version = "2.6.0" +version = "2.7.5" description = "" authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }] requires-python = ">=3.11,<4.0" @@ -11,10 +11,10 @@ dependencies = [ "gunicorn>=21.1.0", "huggingface-hub>=0.20.1,<1.0", "insightface>=0.7.3,<1.0", - "numpy>=2.3.4", + "numpy<2.4.0", "opencv-python-headless>=4.7.0.72,<5.0", "orjson>=3.9.5", - "pillow>=12.1.1,<12.2", + "pillow>=12.2,<12.3", "pydantic>=2.0.0,<3", "pydantic-settings>=2.5.2,<3", "python-multipart>=0.0.6,<1.0", diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index 1bf0b23f36..894acf77f5 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -511,7 +511,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/0a/d2/deb3296d08097fedd [[package]] name = "fastapi" -version = "0.128.8" +version = "0.136.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -520,9 +520,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/72/0df5c58c954742f31a7054e2dd1143bae0b408b7f36b59b85f928f9b456c/fastapi-0.128.8.tar.gz", hash = "sha256:3171f9f328c4a218f0a8d2ba8310ac3a55d1ee12c28c949650288aee25966007", size = 375523, upload-time = "2026-02-11T15:19:36.69Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/d9/e66315807e41e69e7f6a1b42a162dada2f249c5f06ad3f1a95f84ab336ef/fastapi-0.136.0.tar.gz", hash = "sha256:cf08e067cc66e106e102d9ba659463abfac245200752f8a5b7b1e813de4ff73e", size = 396607, upload-time = "2026-04-16T11:47:13.623Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/37/37b07e276f8923c69a5df266bfcb5bac4ba8b55dfe4a126720f8c48681d1/fastapi-0.128.8-py3-none-any.whl", hash = "sha256:5618f492d0fe973a778f8fec97723f598aa9deee495040a8d51aaf3cf123ecf1", size = 103630, upload-time = "2026-02-11T15:19:35.209Z" }, + { url = "https://files.pythonhosted.org/packages/26/a3/0bd5f0cdb0bbc92650e8dc457e9250358411ee5d1b65e42b6632387daf81/fastapi-0.136.0-py3-none-any.whl", hash = "sha256:8793d44ec7378e2be07f8a013cf7f7aa47d6327d0dfe9804862688ec4541a6b4", size = 117556, upload-time = "2026-04-16T11:47:11.922Z" }, ] [[package]] @@ -764,14 +764,14 @@ wheels = [ [[package]] name = "gunicorn" -version = "25.1.0" +version = "25.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/13/ef67f59f6a7896fdc2c1d62b5665c5219d6b0a9a1784938eb9a28e55e128/gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616", size = 594377, upload-time = "2026-02-13T11:09:58.989Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/f4/e78fa054248fab913e2eab0332c6c2cb07421fca1ce56d8fe43b6aef57a4/gunicorn-25.3.0.tar.gz", hash = "sha256:f74e1b2f9f76f6cd1ca01198968bd2dd65830edc24b6e8e4d78de8320e2fe889", size = 634883, upload-time = "2026-03-27T00:00:26.092Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b", size = 197067, upload-time = "2026-02-13T11:09:57.146Z" }, + { url = "https://files.pythonhosted.org/packages/43/c8/8aaf447698c4d59aa853fd318eed300b5c9e44459f242ab8ead6c9c09792/gunicorn-25.3.0-py3-none-any.whl", hash = "sha256:cacea387dab08cd6776501621c295a904fe8e3b7aae9a1a3cbb26f4e7ed54660", size = 208403, upload-time = "2026-03-27T00:00:27.386Z" }, ] [[package]] @@ -898,7 +898,7 @@ wheels = [ [[package]] name = "immich-ml" -version = "2.6.0" +version = "2.7.5" source = { editable = "." } dependencies = [ { name = "aiocache" }, @@ -987,7 +987,7 @@ requires-dist = [ { name = "gunicorn", specifier = ">=21.1.0" }, { name = "huggingface-hub", specifier = ">=0.20.1,<1.0" }, { name = "insightface", specifier = ">=0.7.3,<1.0" }, - { name = "numpy", specifier = ">=2.3.4" }, + { name = "numpy", specifier = "<2.4.0" }, { name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.23.2,<2" }, { name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.23.2,<2" }, { name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.23.2,<2" }, @@ -996,7 +996,7 @@ requires-dist = [ { name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.24.1,<2" }, { name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" }, { name = "orjson", specifier = ">=3.9.5" }, - { name = "pillow", specifier = ">=12.1.1,<12.2" }, + { name = "pillow", specifier = ">=12.2,<12.3" }, { name = "pydantic", specifier = ">=2.0.0,<3" }, { name = "pydantic-settings", specifier = ">=2.5.2,<3" }, { name = "python-multipart", specifier = ">=0.0.6,<1.0" }, @@ -1160,70 +1160,80 @@ wheels = [ [[package]] name = "librt" -version = "0.7.4" +version = "0.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/93/e4/b59bdf1197fdf9888452ea4d2048cdad61aef85eb83e99dc52551d7fdc04/librt-0.7.4.tar.gz", hash = "sha256:3871af56c59864d5fd21d1ac001eb2fb3b140d52ba0454720f2e4a19812404ba", size = 145862, upload-time = "2025-12-15T16:52:43.862Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/64/44089b12d8b4714a7f0e2f33fb19285ba87702d4be0829f20b36ebeeee07/librt-0.7.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3485b9bb7dfa66167d5500ffdafdc35415b45f0da06c75eb7df131f3357b174a", size = 54709, upload-time = "2025-12-15T16:51:16.699Z" }, - { url = "https://files.pythonhosted.org/packages/26/ef/6fa39fb5f37002f7d25e0da4f24d41b457582beea9369eeb7e9e73db5508/librt-0.7.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:188b4b1a770f7f95ea035d5bbb9d7367248fc9d12321deef78a269ebf46a5729", size = 56663, upload-time = "2025-12-15T16:51:17.856Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e4/cbaca170a13bee2469c90df9e47108610b4422c453aea1aec1779ac36c24/librt-0.7.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1b668b1c840183e4e38ed5a99f62fac44c3a3eef16870f7f17cfdfb8b47550ed", size = 161703, upload-time = "2025-12-15T16:51:19.421Z" }, - { url = "https://files.pythonhosted.org/packages/d0/32/0b2296f9cc7e693ab0d0835e355863512e5eac90450c412777bd699c76ae/librt-0.7.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e8f864b521f6cfedb314d171630f827efee08f5c3462bcbc2244ab8e1768cd6", size = 171027, upload-time = "2025-12-15T16:51:20.721Z" }, - { url = "https://files.pythonhosted.org/packages/d8/33/c70b6d40f7342716e5f1353c8da92d9e32708a18cbfa44897a93ec2bf879/librt-0.7.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df7c9def4fc619a9c2ab402d73a0c5b53899abe090e0100323b13ccb5a3dd82", size = 184700, upload-time = "2025-12-15T16:51:22.272Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c8/555c405155da210e4c4113a879d378f54f850dbc7b794e847750a8fadd43/librt-0.7.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f79bc3595b6ed159a1bf0cdc70ed6ebec393a874565cab7088a219cca14da727", size = 180719, upload-time = "2025-12-15T16:51:23.561Z" }, - { url = "https://files.pythonhosted.org/packages/6b/88/34dc1f1461c5613d1b73f0ecafc5316cc50adcc1b334435985b752ed53e5/librt-0.7.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77772a4b8b5f77d47d883846928c36d730b6e612a6388c74cba33ad9eb149c11", size = 174535, upload-time = "2025-12-15T16:51:25.031Z" }, - { url = "https://files.pythonhosted.org/packages/b6/5a/f3fafe80a221626bcedfa9fe5abbf5f04070989d44782f579b2d5920d6d0/librt-0.7.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:064a286e6ab0b4c900e228ab4fa9cb3811b4b83d3e0cc5cd816b2d0f548cb61c", size = 195236, upload-time = "2025-12-15T16:51:26.328Z" }, - { url = "https://files.pythonhosted.org/packages/d8/77/5c048d471ce17f4c3a6e08419be19add4d291e2f7067b877437d482622ac/librt-0.7.4-cp311-cp311-win32.whl", hash = "sha256:42da201c47c77b6cc91fc17e0e2b330154428d35d6024f3278aa2683e7e2daf2", size = 42930, upload-time = "2025-12-15T16:51:27.853Z" }, - { url = "https://files.pythonhosted.org/packages/fb/3b/514a86305a12c3d9eac03e424b07cd312c7343a9f8a52719aa079590a552/librt-0.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:d31acb5886c16ae1711741f22504195af46edec8315fe69b77e477682a87a83e", size = 49240, upload-time = "2025-12-15T16:51:29.037Z" }, - { url = "https://files.pythonhosted.org/packages/ba/01/3b7b1914f565926b780a734fac6e9a4d2c7aefe41f4e89357d73697a9457/librt-0.7.4-cp311-cp311-win_arm64.whl", hash = "sha256:114722f35093da080a333b3834fff04ef43147577ed99dd4db574b03a5f7d170", size = 42613, upload-time = "2025-12-15T16:51:30.194Z" }, - { url = "https://files.pythonhosted.org/packages/f3/e7/b805d868d21f425b7e76a0ea71a2700290f2266a4f3c8357fcf73efc36aa/librt-0.7.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7dd3b5c37e0fb6666c27cf4e2c88ae43da904f2155c4cfc1e5a2fdce3b9fcf92", size = 55688, upload-time = "2025-12-15T16:51:31.571Z" }, - { url = "https://files.pythonhosted.org/packages/59/5e/69a2b02e62a14cfd5bfd9f1e9adea294d5bcfeea219c7555730e5d068ee4/librt-0.7.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c5de1928c486201b23ed0cc4ac92e6e07be5cd7f3abc57c88a9cf4f0f32108", size = 57141, upload-time = "2025-12-15T16:51:32.714Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6b/05dba608aae1272b8ea5ff8ef12c47a4a099a04d1e00e28a94687261d403/librt-0.7.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:078ae52ffb3f036396cc4aed558e5b61faedd504a3c1f62b8ae34bf95ae39d94", size = 165322, upload-time = "2025-12-15T16:51:33.986Z" }, - { url = "https://files.pythonhosted.org/packages/8f/bc/199533d3fc04a4cda8d7776ee0d79955ab0c64c79ca079366fbc2617e680/librt-0.7.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce58420e25097b2fc201aef9b9f6d65df1eb8438e51154e1a7feb8847e4a55ab", size = 174216, upload-time = "2025-12-15T16:51:35.384Z" }, - { url = "https://files.pythonhosted.org/packages/62/ec/09239b912a45a8ed117cb4a6616d9ff508f5d3131bd84329bf2f8d6564f1/librt-0.7.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b719c8730c02a606dc0e8413287e8e94ac2d32a51153b300baf1f62347858fba", size = 189005, upload-time = "2025-12-15T16:51:36.687Z" }, - { url = "https://files.pythonhosted.org/packages/46/2e/e188313d54c02f5b0580dd31476bb4b0177514ff8d2be9f58d4a6dc3a7ba/librt-0.7.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3749ef74c170809e6dee68addec9d2458700a8de703de081c888e92a8b015cf9", size = 183960, upload-time = "2025-12-15T16:51:37.977Z" }, - { url = "https://files.pythonhosted.org/packages/eb/84/f1d568d254518463d879161d3737b784137d236075215e56c7c9be191cee/librt-0.7.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b35c63f557653c05b5b1b6559a074dbabe0afee28ee2a05b6c9ba21ad0d16a74", size = 177609, upload-time = "2025-12-15T16:51:40.584Z" }, - { url = "https://files.pythonhosted.org/packages/5d/43/060bbc1c002f0d757c33a1afe6bf6a565f947a04841139508fc7cef6c08b/librt-0.7.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1ef704e01cb6ad39ad7af668d51677557ca7e5d377663286f0ee1b6b27c28e5f", size = 199269, upload-time = "2025-12-15T16:51:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/ff/7f/708f8f02d8012ee9f366c07ea6a92882f48bd06cc1ff16a35e13d0fbfb08/librt-0.7.4-cp312-cp312-win32.whl", hash = "sha256:c66c2b245926ec15188aead25d395091cb5c9df008d3b3207268cd65557d6286", size = 43186, upload-time = "2025-12-15T16:51:43.149Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a5/4e051b061c8b2509be31b2c7ad4682090502c0a8b6406edcf8c6b4fe1ef7/librt-0.7.4-cp312-cp312-win_amd64.whl", hash = "sha256:71a56f4671f7ff723451f26a6131754d7c1809e04e22ebfbac1db8c9e6767a20", size = 49455, upload-time = "2025-12-15T16:51:44.336Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d2/90d84e9f919224a3c1f393af1636d8638f54925fdc6cd5ee47f1548461e5/librt-0.7.4-cp312-cp312-win_arm64.whl", hash = "sha256:419eea245e7ec0fe664eb7e85e7ff97dcdb2513ca4f6b45a8ec4a3346904f95a", size = 42828, upload-time = "2025-12-15T16:51:45.498Z" }, - { url = "https://files.pythonhosted.org/packages/fe/4d/46a53ccfbb39fd0b493fd4496eb76f3ebc15bb3e45d8c2e695a27587edf5/librt-0.7.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d44a1b1ba44cbd2fc3cb77992bef6d6fdb1028849824e1dd5e4d746e1f7f7f0b", size = 55745, upload-time = "2025-12-15T16:51:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/7f/2b/3ac7f5212b1828bf4f979cf87f547db948d3e28421d7a430d4db23346ce4/librt-0.7.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c9cab4b3de1f55e6c30a84c8cee20e4d3b2476f4d547256694a1b0163da4fe32", size = 57166, upload-time = "2025-12-15T16:51:48.219Z" }, - { url = "https://files.pythonhosted.org/packages/e8/99/6523509097cbe25f363795f0c0d1c6a3746e30c2994e25b5aefdab119b21/librt-0.7.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2857c875f1edd1feef3c371fbf830a61b632fb4d1e57160bb1e6a3206e6abe67", size = 165833, upload-time = "2025-12-15T16:51:49.443Z" }, - { url = "https://files.pythonhosted.org/packages/fe/35/323611e59f8fe032649b4fb7e77f746f96eb7588fcbb31af26bae9630571/librt-0.7.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b370a77be0a16e1ad0270822c12c21462dc40496e891d3b0caf1617c8cc57e20", size = 174818, upload-time = "2025-12-15T16:51:51.015Z" }, - { url = "https://files.pythonhosted.org/packages/41/e6/40fb2bb21616c6e06b6a64022802228066e9a31618f493e03f6b9661548a/librt-0.7.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d05acd46b9a52087bfc50c59dfdf96a2c480a601e8898a44821c7fd676598f74", size = 189607, upload-time = "2025-12-15T16:51:52.671Z" }, - { url = "https://files.pythonhosted.org/packages/32/48/1b47c7d5d28b775941e739ed2bfe564b091c49201b9503514d69e4ed96d7/librt-0.7.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:70969229cb23d9c1a80e14225838d56e464dc71fa34c8342c954fc50e7516dee", size = 184585, upload-time = "2025-12-15T16:51:54.027Z" }, - { url = "https://files.pythonhosted.org/packages/75/a6/ee135dfb5d3b54d5d9001dbe483806229c6beac3ee2ba1092582b7efeb1b/librt-0.7.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4450c354b89dbb266730893862dbff06006c9ed5b06b6016d529b2bf644fc681", size = 178249, upload-time = "2025-12-15T16:51:55.248Z" }, - { url = "https://files.pythonhosted.org/packages/04/87/d5b84ec997338be26af982bcd6679be0c1db9a32faadab1cf4bb24f9e992/librt-0.7.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:adefe0d48ad35b90b6f361f6ff5a1bd95af80c17d18619c093c60a20e7a5b60c", size = 199851, upload-time = "2025-12-15T16:51:56.933Z" }, - { url = "https://files.pythonhosted.org/packages/86/63/ba1333bf48306fe398e3392a7427ce527f81b0b79d0d91618c4610ce9d15/librt-0.7.4-cp313-cp313-win32.whl", hash = "sha256:21ea710e96c1e050635700695095962a22ea420d4b3755a25e4909f2172b4ff2", size = 43249, upload-time = "2025-12-15T16:51:58.498Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8a/de2c6df06cdfa9308c080e6b060fe192790b6a48a47320b215e860f0e98c/librt-0.7.4-cp313-cp313-win_amd64.whl", hash = "sha256:772e18696cf5a64afee908662fbcb1f907460ddc851336ee3a848ef7684c8e1e", size = 49417, upload-time = "2025-12-15T16:51:59.618Z" }, - { url = "https://files.pythonhosted.org/packages/31/66/8ee0949efc389691381ed686185e43536c20e7ad880c122dd1f31e65c658/librt-0.7.4-cp313-cp313-win_arm64.whl", hash = "sha256:52e34c6af84e12921748c8354aa6acf1912ca98ba60cdaa6920e34793f1a0788", size = 42824, upload-time = "2025-12-15T16:52:00.784Z" }, - { url = "https://files.pythonhosted.org/packages/74/81/6921e65c8708eb6636bbf383aa77e6c7dad33a598ed3b50c313306a2da9d/librt-0.7.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4f1ee004942eaaed6e06c087d93ebc1c67e9a293e5f6b9b5da558df6bf23dc5d", size = 55191, upload-time = "2025-12-15T16:52:01.97Z" }, - { url = "https://files.pythonhosted.org/packages/0d/d6/3eb864af8a8de8b39cc8dd2e9ded1823979a27795d72c4eea0afa8c26c9f/librt-0.7.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d854c6dc0f689bad7ed452d2a3ecff58029d80612d336a45b62c35e917f42d23", size = 56898, upload-time = "2025-12-15T16:52:03.356Z" }, - { url = "https://files.pythonhosted.org/packages/49/bc/b1d4c0711fdf79646225d576faee8747b8528a6ec1ceb6accfd89ade7102/librt-0.7.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4f7339d9e445280f23d63dea842c0c77379c4a47471c538fc8feedab9d8d063", size = 163725, upload-time = "2025-12-15T16:52:04.572Z" }, - { url = "https://files.pythonhosted.org/packages/2c/08/61c41cd8f0a6a41fc99ea78a2205b88187e45ba9800792410ed62f033584/librt-0.7.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39003fc73f925e684f8521b2dbf34f61a5deb8a20a15dcf53e0d823190ce8848", size = 172469, upload-time = "2025-12-15T16:52:05.863Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c7/4ee18b4d57f01444230bc18cf59103aeab8f8c0f45e84e0e540094df1df1/librt-0.7.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb15ee29d95875ad697d449fe6071b67f730f15a6961913a2b0205015ca0843", size = 186804, upload-time = "2025-12-15T16:52:07.192Z" }, - { url = "https://files.pythonhosted.org/packages/a1/af/009e8ba3fbf830c936842da048eda1b34b99329f402e49d88fafff6525d1/librt-0.7.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:02a69369862099e37d00765583052a99d6a68af7e19b887e1b78fee0146b755a", size = 181807, upload-time = "2025-12-15T16:52:08.554Z" }, - { url = "https://files.pythonhosted.org/packages/85/26/51ae25f813656a8b117c27a974f25e8c1e90abcd5a791ac685bf5b489a1b/librt-0.7.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ec72342cc4d62f38b25a94e28b9efefce41839aecdecf5e9627473ed04b7be16", size = 175595, upload-time = "2025-12-15T16:52:10.186Z" }, - { url = "https://files.pythonhosted.org/packages/48/93/36d6c71f830305f88996b15c8e017aa8d1e03e2e947b40b55bbf1a34cf24/librt-0.7.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:776dbb9bfa0fc5ce64234b446995d8d9f04badf64f544ca036bd6cff6f0732ce", size = 196504, upload-time = "2025-12-15T16:52:11.472Z" }, - { url = "https://files.pythonhosted.org/packages/08/11/8299e70862bb9d704735bf132c6be09c17b00fbc7cda0429a9df222fdc1b/librt-0.7.4-cp314-cp314-win32.whl", hash = "sha256:0f8cac84196d0ffcadf8469d9ded4d4e3a8b1c666095c2a291e22bf58e1e8a9f", size = 39738, upload-time = "2025-12-15T16:52:12.962Z" }, - { url = "https://files.pythonhosted.org/packages/54/d5/656b0126e4e0f8e2725cd2d2a1ec40f71f37f6f03f135a26b663c0e1a737/librt-0.7.4-cp314-cp314-win_amd64.whl", hash = "sha256:037f5cb6fe5abe23f1dc058054d50e9699fcc90d0677eee4e4f74a8677636a1a", size = 45976, upload-time = "2025-12-15T16:52:14.441Z" }, - { url = "https://files.pythonhosted.org/packages/60/86/465ff07b75c1067da8fa7f02913c4ead096ef106cfac97a977f763783bfb/librt-0.7.4-cp314-cp314-win_arm64.whl", hash = "sha256:a5deebb53d7a4d7e2e758a96befcd8edaaca0633ae71857995a0f16033289e44", size = 39073, upload-time = "2025-12-15T16:52:15.621Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a0/24941f85960774a80d4b3c2aec651d7d980466da8101cae89e8b032a3e21/librt-0.7.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b4c25312c7f4e6ab35ab16211bdf819e6e4eddcba3b2ea632fb51c9a2a97e105", size = 57369, upload-time = "2025-12-15T16:52:16.782Z" }, - { url = "https://files.pythonhosted.org/packages/77/a0/ddb259cae86ab415786c1547d0fe1b40f04a7b089f564fd5c0242a3fafb2/librt-0.7.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:618b7459bb392bdf373f2327e477597fff8f9e6a1878fffc1b711c013d1b0da4", size = 59230, upload-time = "2025-12-15T16:52:18.259Z" }, - { url = "https://files.pythonhosted.org/packages/31/11/77823cb530ab8a0c6fac848ac65b745be446f6f301753b8990e8809080c9/librt-0.7.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1437c3f72a30c7047f16fd3e972ea58b90172c3c6ca309645c1c68984f05526a", size = 183869, upload-time = "2025-12-15T16:52:19.457Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ce/157db3614cf3034b3f702ae5ba4fefda4686f11eea4b7b96542324a7a0e7/librt-0.7.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c96cb76f055b33308f6858b9b594618f1b46e147a4d03a4d7f0c449e304b9b95", size = 194606, upload-time = "2025-12-15T16:52:20.795Z" }, - { url = "https://files.pythonhosted.org/packages/30/ef/6ec4c7e3d6490f69a4fd2803516fa5334a848a4173eac26d8ee6507bff6e/librt-0.7.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28f990e6821204f516d09dc39966ef8b84556ffd648d5926c9a3f681e8de8906", size = 206776, upload-time = "2025-12-15T16:52:22.229Z" }, - { url = "https://files.pythonhosted.org/packages/ad/22/750b37bf549f60a4782ab80e9d1e9c44981374ab79a7ea68670159905918/librt-0.7.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc4aebecc79781a1b77d7d4e7d9fe080385a439e198d993b557b60f9117addaf", size = 203205, upload-time = "2025-12-15T16:52:23.603Z" }, - { url = "https://files.pythonhosted.org/packages/7a/87/2e8a0f584412a93df5faad46c5fa0a6825fdb5eba2ce482074b114877f44/librt-0.7.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:022cc673e69283a42621dd453e2407cf1647e77f8bd857d7ad7499901e62376f", size = 196696, upload-time = "2025-12-15T16:52:24.951Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ca/7bf78fa950e43b564b7de52ceeb477fb211a11f5733227efa1591d05a307/librt-0.7.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2b3ca211ae8ea540569e9c513da052699b7b06928dcda61247cb4f318122bdb5", size = 217191, upload-time = "2025-12-15T16:52:26.194Z" }, - { url = "https://files.pythonhosted.org/packages/d6/49/3732b0e8424ae35ad5c3166d9dd5bcdae43ce98775e0867a716ff5868064/librt-0.7.4-cp314-cp314t-win32.whl", hash = "sha256:8a461f6456981d8c8e971ff5a55f2e34f4e60871e665d2f5fde23ee74dea4eeb", size = 40276, upload-time = "2025-12-15T16:52:27.54Z" }, - { url = "https://files.pythonhosted.org/packages/35/d6/d8823e01bd069934525fddb343189c008b39828a429b473fb20d67d5cd36/librt-0.7.4-cp314-cp314t-win_amd64.whl", hash = "sha256:721a7b125a817d60bf4924e1eec2a7867bfcf64cfc333045de1df7a0629e4481", size = 46772, upload-time = "2025-12-15T16:52:28.653Z" }, - { url = "https://files.pythonhosted.org/packages/36/e9/a0aa60f5322814dd084a89614e9e31139702e342f8459ad8af1984a18168/librt-0.7.4-cp314-cp314t-win_arm64.whl", hash = "sha256:76b2ba71265c0102d11458879b4d53ccd0b32b0164d14deb8d2b598a018e502f", size = 39724, upload-time = "2025-12-15T16:52:29.836Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1e/2ec7afcebcf3efea593d13aee18bbcfdd3a243043d848ebf385055e9f636/librt-0.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671", size = 67155, upload-time = "2026-04-09T16:04:42.933Z" }, + { url = "https://files.pythonhosted.org/packages/18/77/72b85afd4435268338ad4ec6231b3da8c77363f212a0227c1ff3b45e4d35/librt-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d", size = 69916, upload-time = "2026-04-09T16:04:44.042Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/948ea0204fbe2e78add6d46b48330e58d39897e425560674aee302dca81c/librt-0.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6", size = 199635, upload-time = "2026-04-09T16:04:45.5Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cd/894a29e251b296a27957856804cfd21e93c194aa131de8bb8032021be07e/librt-0.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1", size = 211051, upload-time = "2026-04-09T16:04:47.016Z" }, + { url = "https://files.pythonhosted.org/packages/18/8f/dcaed0bc084a35f3721ff2d081158db569d2c57ea07d35623ddaca5cfc8e/librt-0.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882", size = 224031, upload-time = "2026-04-09T16:04:48.207Z" }, + { url = "https://files.pythonhosted.org/packages/03/44/88f6c1ed1132cd418601cc041fbd92fed28b3a09f39de81978e0822d13ff/librt-0.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990", size = 218069, upload-time = "2026-04-09T16:04:50.025Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/7d02e981c2db12188d82b4410ff3e35bfdb844b26aecd02233626f46af2b/librt-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4", size = 224857, upload-time = "2026-04-09T16:04:51.684Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/c77e706b7215ca32e928d47535cf13dbc3d25f096f84ddf8fbc06693e229/librt-0.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb", size = 219865, upload-time = "2026-04-09T16:04:52.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/d1/32b0c1a0eb8461c70c11656c46a29f760b7c7edf3c36d6f102470c17170f/librt-0.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076", size = 218451, upload-time = "2026-04-09T16:04:54.174Z" }, + { url = "https://files.pythonhosted.org/packages/74/d1/adfd0f9c44761b1d49b1bec66173389834c33ee2bd3c7fd2e2367f1942d4/librt-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a", size = 241300, upload-time = "2026-04-09T16:04:55.452Z" }, + { url = "https://files.pythonhosted.org/packages/09/b0/9074b64407712f0003c27f5b1d7655d1438979155f049720e8a1abd9b1a1/librt-0.9.0-cp311-cp311-win32.whl", hash = "sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6", size = 55668, upload-time = "2026-04-09T16:04:56.689Z" }, + { url = "https://files.pythonhosted.org/packages/24/19/40b77b77ce80b9389fb03971431b09b6b913911c38d412059e0b3e2a9ef2/librt-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8", size = 62976, upload-time = "2026-04-09T16:04:57.733Z" }, + { url = "https://files.pythonhosted.org/packages/70/9d/9fa7a64041e29035cb8c575af5f0e3840be1b97b4c4d9061e0713f171849/librt-0.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a", size = 53502, upload-time = "2026-04-09T16:04:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, + { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" }, + { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" }, + { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" }, + { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" }, + { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, + { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, + { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" }, + { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, + { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, + { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" }, + { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" }, + { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, + { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, + { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, ] [[package]] name = "locust" -version = "2.43.3" +version = "2.43.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "configargparse" }, @@ -1243,9 +1253,9 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.12'" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/c5/7d7bd50ac744bc209a4bcbeb74660d7ae450a44441737efe92ee9d8ea6a7/locust-2.43.3.tar.gz", hash = "sha256:b5d2c48f8f7d443e3abdfdd6ec2f7aebff5cd74fab986bcf1e95b375b5c5a54b", size = 1445349, upload-time = "2026-02-12T09:55:34.591Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/be/6df1c778f673e1e2d785f262d20a4e130fdb8e51242466d7ae434b66a587/locust-2.43.4.tar.gz", hash = "sha256:4ace60f07f5fa9bf08d1b64da25915707befca19a790897eed6372656824deee", size = 1434321, upload-time = "2026-04-01T20:43:04.322Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d2/dc5379876d3a481720803653ea4d219f0c26f2d2b37c9243baaa16d0bc79/locust-2.43.3-py3-none-any.whl", hash = "sha256:e032c119b54a9d984cb74a936ee83cfd7d68b3c76c8f308af63d04f11396b553", size = 1463473, upload-time = "2026-02-12T09:55:31.727Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2c/a90d0b6fc476eb0f8e5a705f49e410563450b9b087688cc93a50eab54d63/locust-2.43.4-py3-none-any.whl", hash = "sha256:a4f40403e9f665e0dcb94991d9a8f19317d0d36afe88400833c5fab99ba942ed", size = 1454332, upload-time = "2026-04-01T20:43:02.767Z" }, ] [[package]] @@ -1462,7 +1472,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.19.1" +version = "1.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, @@ -1470,33 +1480,44 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/3d/5b373635b3146264eb7a68d09e5ca11c305bbb058dfffbb47c47daf4f632/mypy-1.20.1.tar.gz", hash = "sha256:6fc3f4ecd52de81648fed1945498bf42fa2993ddfad67c9056df36ae5757f804", size = 3815892, upload-time = "2026-04-13T02:46:51.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, - { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, - { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, - { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, - { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, - { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, - { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, - { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, - { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, - { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, - { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, - { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, + { url = "https://files.pythonhosted.org/packages/82/0d/555ab7453cc4a4a8643b7f21c842b1a84c36b15392061ae7b052ee119320/mypy-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c01eb9bac2c6a962d00f9d23421cd2913840e65bba365167d057bd0b4171a92e", size = 14336012, upload-time = "2026-04-13T02:45:39.935Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/85a28893f7db8a16ebb41d1e9dfcb4475844d06a88480b6639e32a74d6ef/mypy-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55d12ddbd8a9cac5b276878bd534fa39fff5bf543dc6ae18f25d30c8d7d27fca", size = 13224636, upload-time = "2026-04-13T02:45:49.659Z" }, + { url = "https://files.pythonhosted.org/packages/93/41/bd4cd3c2caeb6c448b669222b8cfcbdee4a03b89431527b56fca9e56b6f3/mypy-1.20.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0aa322c1468b6cdfc927a44ce130f79bb44bcd34eb4a009eb9f96571fd80955", size = 13663471, upload-time = "2026-04-13T02:46:20.276Z" }, + { url = "https://files.pythonhosted.org/packages/3e/56/7ee8c471e10402d64b6517ae10434541baca053cffd81090e4097d5609d4/mypy-1.20.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f8bc95899cf676b6e2285779a08a998cc3a7b26f1026752df9d2741df3c79e8", size = 14532344, upload-time = "2026-04-13T02:46:44.205Z" }, + { url = "https://files.pythonhosted.org/packages/b5/95/b37d1fa859a433f6156742e12f62b0bb75af658544fb6dada9363918743a/mypy-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:47c2b90191a870a04041e910277494b0d92f0711be9e524d45c074fe60c00b65", size = 14776670, upload-time = "2026-04-13T02:45:52.481Z" }, + { url = "https://files.pythonhosted.org/packages/03/77/b302e4cb0b80d2bdf6bf4fce5864bb4cbfa461f7099cea544eaf2457df78/mypy-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:9857dc8d2ec1a392ffbda518075beb00ac58859979c79f9e6bdcb7277082c2f2", size = 10816524, upload-time = "2026-04-13T02:45:37.711Z" }, + { url = "https://files.pythonhosted.org/packages/7f/21/d969d7a68eb964993ebcc6170d5ecaf0cf65830c58ac3344562e16dc42a9/mypy-1.20.1-cp311-cp311-win_arm64.whl", hash = "sha256:09d8df92bb25b6065ab91b178da843dda67b33eb819321679a6e98a907ce0e10", size = 9750419, upload-time = "2026-04-13T02:45:08.542Z" }, + { url = "https://files.pythonhosted.org/packages/69/1b/75a7c825a02781ca10bc2f2f12fba2af5202f6d6005aad8d2d1f264d8d78/mypy-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:36ee2b9c6599c230fea89bbd79f401f9f9f8e9fcf0c777827789b19b7da90f51", size = 14494077, upload-time = "2026-04-13T02:45:55.085Z" }, + { url = "https://files.pythonhosted.org/packages/b0/54/5e5a569ea5c2b4d48b729fb32aa936eeb4246e4fc3e6f5b3d36a2dfbefb9/mypy-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fba3fb0968a7b48806b0c90f38d39296f10766885a94c83bd21399de1e14eb28", size = 13319495, upload-time = "2026-04-13T02:45:29.674Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a4/a1945b19f33e91721b59deee3abb484f2fa5922adc33bb166daf5325d76d/mypy-1.20.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef1415a637cd3627d6304dfbeddbadd21079dafc2a8a753c477ce4fc0c2af54f", size = 13696948, upload-time = "2026-04-13T02:46:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c6/75e969781c2359b2f9c15b061f28ec6d67c8b61865ceda176e85c8e7f2de/mypy-1.20.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef3461b1ad5cd446e540016e90b5984657edda39f982f4cc45ca317b628f5a37", size = 14706744, upload-time = "2026-04-13T02:46:00.482Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6e/b221b1de981fc4262fe3e0bf9ec272d292dfe42394a689c2d49765c144c4/mypy-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:542dd63c9e1339b6092eb25bd515f3a32a1453aee8c9521d2ddb17dacd840237", size = 14949035, upload-time = "2026-04-13T02:45:06.021Z" }, + { url = "https://files.pythonhosted.org/packages/ca/4b/298ba2de0aafc0da3ff2288da06884aae7ba6489bc247c933f87847c41b3/mypy-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d55c7cd8ca22e31f93af2a01160a9e95465b5878de23dba7e48116052f20a8d", size = 10883216, upload-time = "2026-04-13T02:45:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f9/5e25b8f0b8cb92f080bfed9c21d3279b2a0b6a601cdca369a039ba84789d/mypy-1.20.1-cp312-cp312-win_arm64.whl", hash = "sha256:f5b84a79070586e0d353ee07b719d9d0a4aa7c8ee90c0ea97747e98cbe193019", size = 9814299, upload-time = "2026-04-13T02:45:21.934Z" }, + { url = "https://files.pythonhosted.org/packages/21/e8/ef0991aa24c8f225df10b034f3c2681213cb54cf247623c6dec9a5744e70/mypy-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f3886c03e40afefd327bd70b3f634b39ea82e87f314edaa4d0cce4b927ddcc1", size = 14500739, upload-time = "2026-04-13T02:46:05.442Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/416ebec3047636ed89fa871dc8c54bf05e9e20aa9499da59790d7adb312d/mypy-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e860eb3904f9764e83bafd70c8250bdffdc7dde6b82f486e8156348bf7ceb184", size = 13314735, upload-time = "2026-04-13T02:46:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/10/1e/1505022d9c9ac2e014a384eb17638fb37bf8e9d0a833ea60605b66f8f7ba/mypy-1.20.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4b5aac6e785719da51a84f5d09e9e843d473170a9045b1ea7ea1af86225df4b", size = 13704356, upload-time = "2026-04-13T02:45:19.773Z" }, + { url = "https://files.pythonhosted.org/packages/98/91/275b01f5eba5c467a3318ec214dd865abb66e9c811231c8587287b92876a/mypy-1.20.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f37b6cd0fe2ad3a20f05ace48ca3523fc52ff86940e34937b439613b6854472e", size = 14696420, upload-time = "2026-04-13T02:45:24.205Z" }, + { url = "https://files.pythonhosted.org/packages/a1/57/b3779e134e1b7250d05f874252780d0a88c068bc054bcff99ca20a3a2986/mypy-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4bbb0f6b54ce7cc350ef4a770650d15fa70edd99ad5267e227133eda9c94218", size = 14936093, upload-time = "2026-04-13T02:45:32.087Z" }, + { url = "https://files.pythonhosted.org/packages/be/33/81b64991b0f3f278c3b55c335888794af190b2d59031a5ad1401bcb69f1e/mypy-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:c3dc20f8ec76eecd77148cdd2f1542ed496e51e185713bf488a414f862deb8f2", size = 10889659, upload-time = "2026-04-13T02:46:02.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fd/7adcb8053572edf5ef8f3db59599dfeeee3be9cc4c8c97e2d28f66f42ac5/mypy-1.20.1-cp313-cp313-win_arm64.whl", hash = "sha256:a9d62bbac5d6d46718e2b0330b25e6264463ed832722b8f7d4440ff1be3ca895", size = 9815515, upload-time = "2026-04-13T02:46:32.103Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/db831e84c81d57d4886d99feee14e372f64bbec6a9cb1a88a19e243f2ef5/mypy-1.20.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:12927b9c0ed794daedcf1dab055b6c613d9d5659ac511e8d936d96f19c087d12", size = 14483064, upload-time = "2026-04-13T02:45:26.901Z" }, + { url = "https://files.pythonhosted.org/packages/d5/82/74e62e7097fa67da328ac8ece8de09133448c04d20ddeaeba251a3000f01/mypy-1.20.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:752507dd481e958b2c08fc966d3806c962af5a9433b5bf8f3bdd7175c20e34fe", size = 13335694, upload-time = "2026-04-13T02:46:12.514Z" }, + { url = "https://files.pythonhosted.org/packages/74/c4/97e9a0abe4f3cdbbf4d079cb87a03b786efeccf5bf2b89fe4f96939ab2e6/mypy-1.20.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c614655b5a065e56274c6cbbe405f7cf7e96c0654db7ba39bc680238837f7b08", size = 13726365, upload-time = "2026-04-13T02:45:17.422Z" }, + { url = "https://files.pythonhosted.org/packages/d7/aa/a19d884a8d28fcd3c065776323029f204dbc774e70ec9c85eba228b680de/mypy-1.20.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c3f6221a76f34d5100c6d35b3ef6b947054123c3f8d6938a4ba00b1308aa572", size = 14693472, upload-time = "2026-04-13T02:46:41.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/cc9324bd21cf786592b44bf3b5d224b3923c1230ec9898d508d00241d465/mypy-1.20.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4bdfc06303ac06500af71ea0cdbe995c502b3c9ba32f3f8313523c137a25d1b6", size = 14919266, upload-time = "2026-04-13T02:46:28.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dc/779abb25a8c63e8f44bf5a336217fa92790fa17e0c40e0c725d10cb01bbd/mypy-1.20.1-cp314-cp314-win_amd64.whl", hash = "sha256:0131edd7eba289973d1ba1003d1a37c426b85cdef76650cd02da6420898a5eb3", size = 11049713, upload-time = "2026-04-13T02:45:57.673Z" }, + { url = "https://files.pythonhosted.org/packages/28/08/4172be2ad7de9119b5a92ca36abbf641afdc5cb1ef4ae0c3a8182f29674f/mypy-1.20.1-cp314-cp314-win_arm64.whl", hash = "sha256:33f02904feb2c07e1fdf7909026206396c9deeb9e6f34d466b4cfedb0aadbbe4", size = 9999819, upload-time = "2026-04-13T02:46:35.039Z" }, + { url = "https://files.pythonhosted.org/packages/2d/af/af9e46b0c8eabbce9fc04a477564170f47a1c22b308822282a59b7ff315f/mypy-1.20.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:168472149dd8cc505c98cefd21ad77e4257ed6022cd5ed2fe2999bed56977a5a", size = 15547508, upload-time = "2026-04-13T02:46:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/a7/cd/39c9e4ad6ba33e069e5837d772a9e6c304b4a5452a14a975d52b36444650/mypy-1.20.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eb674600309a8f22790cca883a97c90299f948183ebb210fbef6bcee07cb1986", size = 14399557, upload-time = "2026-04-13T02:46:10.021Z" }, + { url = "https://files.pythonhosted.org/packages/83/c1/3fd71bdc118ffc502bf57559c909927bb7e011f327f7bb8e0488e98a5870/mypy-1.20.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef2b2e4cc464ba9795459f2586923abd58a0055487cbe558cb538ea6e6bc142a", size = 15045789, upload-time = "2026-04-13T02:45:10.81Z" }, + { url = "https://files.pythonhosted.org/packages/8e/73/6f07ff8b57a7d7b3e6e5bf34685d17632382395c8bb53364ec331661f83e/mypy-1.20.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee461d396dd46b3f0ed5a098dbc9b8860c81c46ad44fa071afcfbc149f167c9", size = 15850795, upload-time = "2026-04-13T02:45:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e2/f7dffec1c7767078f9e9adf0c786d1fe0ff30964a77eb213c09b8b58cb76/mypy-1.20.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e364926308b3e66f1361f81a566fc1b2f8cd47fc8525e8136d4058a65a4b4f02", size = 16088539, upload-time = "2026-04-13T02:46:17.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/76/e0dee71035316e75a69d73aec2f03c39c21c967b97e277fd0ef8fd6aec66/mypy-1.20.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a0c17fbd746d38c70cbc42647cfd884f845a9708a4b160a8b4f7e70d41f4d7fa", size = 12575567, upload-time = "2026-04-13T02:45:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/7ed43c9d9c3d1468f86605e323a5d97e411a448790a00f07e779f3211a46/mypy-1.20.1-cp314-cp314t-win_arm64.whl", hash = "sha256:db2cb89654626a912efda69c0d5c1d22d948265e2069010d3dde3abf751c7d08", size = 10378823, upload-time = "2026-04-13T02:45:13.35Z" }, + { url = "https://files.pythonhosted.org/packages/d8/28/926bd972388e65a39ee98e188ccf67e81beb3aacfd5d6b310051772d974b/mypy-1.20.1-py3-none-any.whl", hash = "sha256:1aae28507f253fe82d883790d1c0a0d35798a810117c88184097fe8881052f06", size = 2636553, upload-time = "2026-04-13T02:46:30.45Z" }, ] [[package]] @@ -1519,81 +1540,83 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.2" +version = "2.3.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, - { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, - { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, - { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, - { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, - { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, - { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, - { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, - { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, - { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, - { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, - { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, - { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, - { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, - { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, - { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, - { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, - { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, - { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, - { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, - { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, - { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, - { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, - { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, - { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, - { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, - { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, - { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, - { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, - { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, - { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, - { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, - { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, - { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, - { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, - { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, - { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, - { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, - { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, - { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, - { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, - { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, - { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, - { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, - { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, - { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, - { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, - { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, - { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, - { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, - { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, - { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, - { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, - { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, + { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, + { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, + { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, + { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, ] [[package]] @@ -1648,7 +1671,7 @@ wheels = [ [[package]] name = "onnxruntime" -version = "1.24.1" +version = "1.24.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flatbuffers" }, @@ -1658,31 +1681,35 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/88/d9757c62a0f96b5193f8d447a141eefd14498c404cc5caf1a6f3233cf102/onnxruntime-1.24.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:79b3119ab9f4f3817062e6dbe7f4a44937de93905e3a31ba34313d18cb49e7be", size = 17212018, upload-time = "2026-02-05T17:32:13.986Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/b3305c39144e19dbe8791802076b29b4b592b09de03d0e340c1314bfd408/onnxruntime-1.24.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86bc43e922b1f581b3de26a3dc402149c70e5542fceb5bec6b3a85542dbeb164", size = 15018703, upload-time = "2026-02-05T17:30:53.846Z" }, - { url = "https://files.pythonhosted.org/packages/94/d6/d273b75fe7825ea3feed321dd540aef33d8a1380ddd8ac3bb70a8ed000fe/onnxruntime-1.24.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cabe71ca14dcfbf812d312aab0a704507ac909c137ee6e89e4908755d0fc60e", size = 17096352, upload-time = "2026-02-05T17:31:29.057Z" }, - { url = "https://files.pythonhosted.org/packages/21/3f/0616101a3938bfe2918ea60b581a9bbba61ffc255c63388abb0885f7ce18/onnxruntime-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:3273c330f5802b64b4103e87b5bbc334c0355fff1b8935d8910b0004ce2f20c8", size = 12493235, upload-time = "2026-02-05T17:32:04.451Z" }, - { url = "https://files.pythonhosted.org/packages/c8/30/437de870e4e1c6d237a2ca5e11f54153531270cb5c745c475d6e3d5c5dcf/onnxruntime-1.24.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7307aab9e2e879c0171f37e0eb2808a5b4aec7ba899bb17c5f0cedfc301a8ac2", size = 17211043, upload-time = "2026-02-05T17:32:16.909Z" }, - { url = "https://files.pythonhosted.org/packages/21/60/004401cd86525101ad8aa9eec301327426555d7a77fac89fd991c3c7aae6/onnxruntime-1.24.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:780add442ce2d4175fafb6f3102cdc94243acffa3ab16eacc03dd627cc7b1b54", size = 15016224, upload-time = "2026-02-05T17:30:56.791Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a1/43ad01b806a1821d1d6f98725edffcdbad54856775643718e9124a09bfbe/onnxruntime-1.24.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6119526eda12613f0d0498e2ae59563c247c370c9cef74c2fc93133dde157", size = 17098191, upload-time = "2026-02-05T17:31:31.87Z" }, - { url = "https://files.pythonhosted.org/packages/ff/37/5beb65270864037d5c8fb25cfe6b23c48b618d1f4d06022d425cbf29bd9c/onnxruntime-1.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:df0af2f1cfcfff9094971c7eb1d1dfae7ccf81af197493c4dc4643e4342c0946", size = 12493108, upload-time = "2026-02-05T17:32:07.076Z" }, - { url = "https://files.pythonhosted.org/packages/95/77/7172ecfcbdabd92f338e694f38c325f6fab29a38fa0a8c3d1c85b9f4617c/onnxruntime-1.24.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:82e367770e8fba8a87ba9f4c04bb527e6d4d7204540f1390f202c27a3b759fb4", size = 17211381, upload-time = "2026-02-05T17:31:09.601Z" }, - { url = "https://files.pythonhosted.org/packages/79/5b/532a0d75b93bbd0da0e108b986097ebe164b84fbecfdf2ddbf7c8a3a2e83/onnxruntime-1.24.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1099f3629832580fedf415cfce2462a56cc9ca2b560d6300c24558e2ac049134", size = 15016000, upload-time = "2026-02-05T17:31:00.116Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b5/40606c7bce0702975a077bc6668cd072cd77695fc5c0b3fcf59bdb1fe65e/onnxruntime-1.24.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6361dda4270f3939a625670bd67ae0982a49b7f923207450e28433abc9c3a83b", size = 17097637, upload-time = "2026-02-05T17:31:34.787Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a0/9e8f7933796b466241b934585723c700d8fb6bde2de856e65335193d7c93/onnxruntime-1.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:bd1e4aefe73b6b99aa303cd72562ab6de3cccb09088100f8ad1c974be13079c7", size = 12492467, upload-time = "2026-02-05T17:32:09.834Z" }, - { url = "https://files.pythonhosted.org/packages/fb/8a/ee07d86e35035f9fed42497af76435f5a613d4e8b6c537ea0f8ef9fa85da/onnxruntime-1.24.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88a2b54dca00c90fca6303eedf13d49b5b4191d031372c2e85f5cffe4d86b79e", size = 15025407, upload-time = "2026-02-05T17:31:02.251Z" }, - { url = "https://files.pythonhosted.org/packages/fd/9e/ab3e1dda4b126313d240e1aaa87792ddb1f5ba6d03ca2f093a7c4af8c323/onnxruntime-1.24.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2dfbba602da840615ed5b431facda4b3a43b5d8276cf9e0dbf13d842df105838", size = 17099810, upload-time = "2026-02-05T17:31:37.537Z" }, - { url = "https://files.pythonhosted.org/packages/87/23/167d964414cee2af9c72af323b28d2c4cb35beed855c830a23f198265c79/onnxruntime-1.24.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:890c503ca187bc883c3aa72c53f2a604ec8e8444bdd1bf6ac243ec6d5e085202", size = 17214004, upload-time = "2026-02-05T17:31:11.917Z" }, - { url = "https://files.pythonhosted.org/packages/b4/24/6e5558fdd51027d6830cf411bc003ae12c64054826382e2fab89e99486a0/onnxruntime-1.24.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da1b84b3bdeec543120df169e5e62a1445bf732fc2c7fb036c2f8a4090455e8", size = 15017034, upload-time = "2026-02-05T17:31:04.331Z" }, - { url = "https://files.pythonhosted.org/packages/91/d4/3cb1c9eaae1103265ed7eb00a3eaeb0d9ba51dc88edc398b7071c9553bed/onnxruntime-1.24.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:557753ec345efa227c6a65139f3d29c76330fcbd54cc10dd1b64232ebb939c13", size = 17097531, upload-time = "2026-02-05T17:31:40.303Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/4522b199c12db7c5b46aaf265ee0d741abe65ea912f6c0aaa2cc18a4654d/onnxruntime-1.24.1-cp314-cp314-win_amd64.whl", hash = "sha256:ea4942104805e868f3ddddfa1fbb58b04503a534d489ab2d1452bbfa345c78c2", size = 12795556, upload-time = "2026-02-05T17:32:11.886Z" }, - { url = "https://files.pythonhosted.org/packages/a1/53/3b8969417276b061ff04502ccdca9db4652d397abbeb06c9f6ae05cec9ca/onnxruntime-1.24.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea8963a99e0f10489acdf00ef3383c3232b7e44aa497b063c63be140530d9f85", size = 15025434, upload-time = "2026-02-05T17:31:06.942Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a2/cfcf009eb38d90cc628c087b6506b3dfe1263387f3cbbf8d272af4fef957/onnxruntime-1.24.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34488aa760fb5c2e6d06a7ca9241124eb914a6a06f70936a14c669d1b3df9598", size = 17099815, upload-time = "2026-02-05T17:31:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/60/69/6c40720201012c6af9aa7d4ecdd620e521bd806dc6269d636fdd5c5aeebe/onnxruntime-1.24.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bdfce8e9a6497cec584aab407b71bf697dac5e1b7b7974adc50bf7533bdb3a2", size = 17332131, upload-time = "2026-03-17T22:05:49.005Z" }, + { url = "https://files.pythonhosted.org/packages/38/e9/8c901c150ce0c368da38638f44152fb411059c0c7364b497c9e5c957321a/onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:046ff290045a387676941a02a8ae5c3ebec6b4f551ae228711968c4a69d8f6b7", size = 15152472, upload-time = "2026-03-17T22:03:26.176Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b6/7a4df417cdd01e8f067a509e123ac8b31af450a719fa7ed81787dd6057ec/onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e54ad52e61d2d4618dcff8fa1480ac66b24ee2eab73331322db1049f11ccf330", size = 17222993, upload-time = "2026-03-17T22:04:34.485Z" }, + { url = "https://files.pythonhosted.org/packages/dd/59/8febe015f391aa1757fa5ba82c759ea4b6c14ef970132efb5e316665ba61/onnxruntime-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b43b63eb24a2bc8fc77a09be67587a570967a412cccb837b6245ccb546691153", size = 12594863, upload-time = "2026-03-17T22:05:38.749Z" }, + { url = "https://files.pythonhosted.org/packages/32/84/4155fcd362e8873eb6ce305acfeeadacd9e0e59415adac474bea3d9281bb/onnxruntime-1.24.4-cp311-cp311-win_arm64.whl", hash = "sha256:e26478356dba25631fb3f20112e345f8e8bf62c499bb497e8a559f7d69cf7e7b", size = 12259895, upload-time = "2026-03-17T22:05:28.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/38/31db1b232b4ba960065a90c1506ad7a56995cd8482033184e97fadca17cc/onnxruntime-1.24.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cad1c2b3f455c55678ab2a8caa51fb420c25e6e3cf10f4c23653cdabedc8de78", size = 17341875, upload-time = "2026-03-17T22:05:51.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/60/c4d1c8043eb42f8a9aa9e931c8c293d289c48ff463267130eca97d13357f/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a5c5a544b22f90859c88617ecb30e161ee3349fcc73878854f43d77f00558b5", size = 15172485, upload-time = "2026-03-17T22:03:32.182Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/5b68110e0460d73fad814d5bd11c7b1ddcce5c37b10177eb264d6a36e331/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d640eb9f3782689b55cfa715094474cd5662f2f137be6a6f847a594b6e9705c", size = 17244912, upload-time = "2026-03-17T22:04:37.251Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f4/6b89e297b93704345f0f3f8c62229bee323ef25682a3f9b4f89a39324950/onnxruntime-1.24.4-cp312-cp312-win_amd64.whl", hash = "sha256:535b29475ca42b593c45fbb2152fbf1cdf3f287315bf650e6a724a0a1d065cdb", size = 12596856, upload-time = "2026-03-17T22:05:41.224Z" }, + { url = "https://files.pythonhosted.org/packages/43/06/8b8ec6e9e6a474fcd5d772453f627ad4549dfe3ab8c0bf70af5afcde551b/onnxruntime-1.24.4-cp312-cp312-win_arm64.whl", hash = "sha256:e6214096e14b7b52e3bee1903dc12dc7ca09cb65e26664668a4620cc5e6f9a90", size = 12270275, upload-time = "2026-03-17T22:05:31.132Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f0/8a21ec0a97e40abb7d8da1e8b20fb9e1af509cc6d191f6faa75f73622fb2/onnxruntime-1.24.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e99a48078baaefa2b50fe5836c319499f71f13f76ed32d0211f39109147a49e0", size = 17341922, upload-time = "2026-03-17T22:03:56.364Z" }, + { url = "https://files.pythonhosted.org/packages/8b/25/d7908de8e08cee9abfa15b8aa82349b79733ae5865162a3609c11598805d/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4aaed1e5e1aaacf2343c838a30a7c3ade78f13eeb16817411f929d04040a13", size = 15172290, upload-time = "2026-03-17T22:03:37.124Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/105ec27a78c5aa0154a7c0cd8c41c19a97799c3b12fc30392928997e3be3/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e30c972bc02e072911aabb6891453ec73795386c0af2b761b65444b8a4c4745f", size = 17244738, upload-time = "2026-03-17T22:04:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/05/fb/a592736d968c2f58e12de4d52088dda8e0e724b26ad5c0487263adb45875/onnxruntime-1.24.4-cp313-cp313-win_amd64.whl", hash = "sha256:3b6ba8b0181a3aa88edab00eb01424ffc06f42e71095a91186c2249415fcff93", size = 12597435, upload-time = "2026-03-17T22:05:43.826Z" }, + { url = "https://files.pythonhosted.org/packages/ad/04/ae2479e9841b64bd2eb44f8a64756c62593f896514369a11243b1b86ca5c/onnxruntime-1.24.4-cp313-cp313-win_arm64.whl", hash = "sha256:71d6a5c1821d6e8586a024000ece458db8f2fc0ecd050435d45794827ce81e19", size = 12269852, upload-time = "2026-03-17T22:05:33.353Z" }, + { url = "https://files.pythonhosted.org/packages/b4/af/a479a536c4398ffaf49fbbe755f45d5b8726bdb4335ab31b537f3d7149b8/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1700f559c8086d06b2a4d5de51e62cb4ff5e2631822f71a36db8c72383db71ee", size = 15176861, upload-time = "2026-03-17T22:03:40.143Z" }, + { url = "https://files.pythonhosted.org/packages/be/13/19f5da70c346a76037da2c2851ecbf1266e61d7f0dcdb887c667210d4608/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c74e268dc808e61e63784d43f9ddcdaf50a776c2819e8bd1d1b11ef64bf7e36", size = 17247454, upload-time = "2026-03-17T22:04:46.643Z" }, + { url = "https://files.pythonhosted.org/packages/89/db/b30dbbd6037847b205ab75d962bc349bf1e46d02a65b30d7047a6893ffd6/onnxruntime-1.24.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:fbff2a248940e3398ae78374c5a839e49a2f39079b488bc64439fa0ec327a3e4", size = 17343300, upload-time = "2026-03-17T22:03:59.223Z" }, + { url = "https://files.pythonhosted.org/packages/61/88/1746c0e7959961475b84c776d35601a21d445f463c93b1433a409ec3e188/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2b7969e72d8cb53ffc88ab6d49dd5e75c1c663bda7be7eb0ece192f127343d1", size = 15175936, upload-time = "2026-03-17T22:03:43.671Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ba/4699cde04a52cece66cbebc85bd8335a0d3b9ad485abc9a2e15946a1349d/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14ed1f197fab812b695a5eaddb536c635e58a2fbbe50a517c78f082cc6ce9177", size = 17246432, upload-time = "2026-03-17T22:04:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/4590910841bb28bd3b4b388a9efbedf4e2d2cca99ddf0c863642b4e87814/onnxruntime-1.24.4-cp314-cp314-win_amd64.whl", hash = "sha256:311e309f573bf3c12aa5723e23823077f83d5e412a18499d4485c7eb41040858", size = 12903276, upload-time = "2026-03-17T22:05:46.349Z" }, + { url = "https://files.pythonhosted.org/packages/7f/6f/60e2c0acea1e1ac09b3e794b5a19c166eebf91c0b860b3e6db8e74983fda/onnxruntime-1.24.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f0b910e86b759a4732663ec61fd57ac42ee1b0066f68299de164220b660546d", size = 12594365, upload-time = "2026-03-17T22:05:35.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/0c05d10f8f6c40fe0912ebec0d5a33884aaa2af2053507e864dab0883208/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa12ddc54c9c4594073abcaa265cd9681e95fb89dae982a6f508a794ca42e661", size = 15176889, upload-time = "2026-03-17T22:03:48.021Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1d/1666dc64e78d8587d168fec4e3b7922b92eb286a2ddeebcf6acb55c7dc82/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1cc6a518255f012134bc791975a6294806be9a3b20c4a54cca25194c90cf731", size = 17247021, upload-time = "2026-03-17T22:04:52.377Z" }, ] [[package]] name = "onnxruntime-gpu" -version = "1.24.1" +version = "1.24.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flatbuffers" }, @@ -1692,16 +1719,16 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/c7/07d06175f1124fc89e8b7da30d70eb8e0e1400d90961ae1cbea9da69e69b/onnxruntime_gpu-1.24.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac4bfc90c376516b13d709764ab257e4e3d78639bf6a2ccfc826e9db4a5c7ddf", size = 252616647, upload-time = "2026-02-05T17:24:02.993Z" }, - { url = "https://files.pythonhosted.org/packages/8c/9a/47c2a873bf5fc307cda696e8a8cb54b7c709f5a4b3f9e2b4a636066a63c2/onnxruntime_gpu-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:ccd800875cb6c04ce623154c7fa312da21631ef89a9543c9a21593817cfa3473", size = 207089749, upload-time = "2026-02-05T17:23:59.5Z" }, - { url = "https://files.pythonhosted.org/packages/db/a8/fb1a36a052321a839cc9973f6cfd630709412a24afff2d7315feb3efc4b8/onnxruntime_gpu-1.24.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:710bf83751e6761584ad071102af3cbffd4b42bb77b2e3caacfb54ffbaa0666b", size = 252628733, upload-time = "2026-02-05T17:24:12.926Z" }, - { url = "https://files.pythonhosted.org/packages/52/65/48f694b81a963f3ee575041d5f2879b15268f5e7e14d90c3e671836c9646/onnxruntime_gpu-1.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:b128a42b3fa098647765ba60c2af9d4bf839181307cfac27da649364feb37f7b", size = 207089008, upload-time = "2026-02-05T17:24:07.126Z" }, - { url = "https://files.pythonhosted.org/packages/7a/e7/4e19062e95d3701c0d32c228aa848ba4a1cc97651e53628d978dba8e1267/onnxruntime_gpu-1.24.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db9acb0d0e59d93b4fa6b7fd44284ece4408d0acee73235d43ed343f8cee7ee5", size = 252629216, upload-time = "2026-02-05T17:24:24.604Z" }, - { url = "https://files.pythonhosted.org/packages/c4/82/223d7120d8a98b07c104ddecfb0cc2536188e566a4e9c2dee7572453f89c/onnxruntime_gpu-1.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:59fdb40743f0722f3b859209f649ea160ca6bb42799e43f49b70a3ec5fc8c4ad", size = 207089285, upload-time = "2026-02-05T17:24:18.497Z" }, - { url = "https://files.pythonhosted.org/packages/ac/82/3159e57f09d7e6c8ad47d8ba8d5bd7494f383bc1071481cf38c9c8142bf9/onnxruntime_gpu-1.24.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88ca04e1dffea2d4c3c79cf4de7f429e99059d085f21b3e775a8d36380cd5186", size = 252633977, upload-time = "2026-02-05T17:24:33.568Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b4/51ad0ab878ff1456a831a0566b4db982a904e22f138e4b2c5f021bac517f/onnxruntime_gpu-1.24.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ced66900b1f48bddb62b5233925c3b56f8e008e2c34ebf8c060b20cae5842bcf", size = 252629039, upload-time = "2026-02-05T17:24:43.551Z" }, - { url = "https://files.pythonhosted.org/packages/9c/46/336d4e09a6af66532eedde5c8f03a73eaa91a046b408522259ab6a604363/onnxruntime_gpu-1.24.1-cp314-cp314-win_amd64.whl", hash = "sha256:129f6ae8b331a6507759597cd317b23e94aed6ead1da951f803c3328f2990b0c", size = 209487551, upload-time = "2026-02-05T17:24:26.373Z" }, - { url = "https://files.pythonhosted.org/packages/6a/94/a3b20276261f5e64dbd72bda656af988282cff01f18c2685953600e2f810/onnxruntime_gpu-1.24.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2cee7e12b0f4813c62f9a48df83fd01d066cc970400c832252cf3c155a6957", size = 252633096, upload-time = "2026-02-05T17:24:53.248Z" }, + { url = "https://files.pythonhosted.org/packages/9f/13/e080d758f2b60f71abe518c707135fb121d6a3019e0761ead89b5283ac3d/onnxruntime_gpu-1.24.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a698659271c28220b3f56fe9b63f70eae3b3c36afa544201bf750b929a36dc", size = 252761835, upload-time = "2026-03-17T22:03:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/d2/07/036825cbe30f91ea8574a18a759beccd0ea31b7b71e17f6a9ee9304b51d2/onnxruntime_gpu-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a799a16e5f1ff4d6a9e5f72d750849ab0fe534da8d323ae4a5d8d8bb7daeca8", size = 207193563, upload-time = "2026-03-17T21:58:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2c/5b3fd4748cf7ed291eae541a37e426efc20ea04cb6e6a05768304ab0aa41/onnxruntime_gpu-1.24.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb0e38f0c1ef3b76ae0081c8e51eed20dd8925aa916f0fc6f9b8b17d05610e99", size = 252765531, upload-time = "2026-03-17T22:03:57.528Z" }, + { url = "https://files.pythonhosted.org/packages/f2/86/70cecfdab1e963cc7f8c11e72040dfcd5cff85b1de2de74deba9611e0059/onnxruntime_gpu-1.24.4-cp312-cp312-win_amd64.whl", hash = "sha256:da5c1e327d8e119a831be2790e69f93cf6daab9145ed0aca7577f412a620f709", size = 207197978, upload-time = "2026-03-17T21:58:38.43Z" }, + { url = "https://files.pythonhosted.org/packages/be/4e/56d11203d7a35e7d6a5ea735f5fecb8673537038c07323e8d3090a896547/onnxruntime_gpu-1.24.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbdaa73f9055fb2a177425edbed651a1843a6239f9d5430e284f4e5f65440a33", size = 252763446, upload-time = "2026-03-17T22:04:09.515Z" }, + { url = "https://files.pythonhosted.org/packages/fa/bc/35f3a37226d7a28c84b8b456f52237ccd39eb7111114bcf9ac340178e1ec/onnxruntime_gpu-1.24.4-cp313-cp313-win_amd64.whl", hash = "sha256:6be8bf2048777c517fca33eb61e114969fa326619feaa789d8c75f24337ea762", size = 207198775, upload-time = "2026-03-17T21:58:48.768Z" }, + { url = "https://files.pythonhosted.org/packages/37/83/0c851882051b38f245f44b4a51d6232b95b8cd5d334b2c1260f2d796834f/onnxruntime_gpu-1.24.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4b348a078ced73fc577d21b83992fd2187edd10c233729c8d01b000b8543525", size = 252774594, upload-time = "2026-03-17T22:04:24.957Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5b/82b27f766b64f97c9a98b772dc07b608e900bd2faafdfa176b86d20be7f8/onnxruntime_gpu-1.24.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af9dd7ef92d94c75e5523cf070e180f3d8cdbb2fc007dcea97ba71b03e3b96d6", size = 252765395, upload-time = "2026-03-17T22:04:37.305Z" }, + { url = "https://files.pythonhosted.org/packages/5d/95/fa8c48e03790c979167d08164b34a8442c7074bca4c7253b4455497025de/onnxruntime_gpu-1.24.4-cp314-cp314-win_amd64.whl", hash = "sha256:4dde3d2f1039060c42b12fd446fc0da5b836cc65dceb4020ca60a04cffa1d90d", size = 209597109, upload-time = "2026-03-17T21:58:58.136Z" }, + { url = "https://files.pythonhosted.org/packages/1a/98/7707edefcecf69d6c45b83a83f13ac58257017b4eaf58772668d302f849f/onnxruntime_gpu-1.24.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:097c6f53e99ee35f21d0fdba76ca283b92465a0e364c6f0209cb9653c424e2a4", size = 252776951, upload-time = "2026-03-17T22:04:49.715Z" }, ] [[package]] @@ -1779,70 +1806,70 @@ wheels = [ [[package]] name = "orjson" -version = "3.11.7" +version = "3.11.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, - { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, - { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, - { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, - { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, - { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, - { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, - { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, - { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, - { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, - { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, - { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, - { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, - { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, - { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, - { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, - { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, - { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, - { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, - { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, - { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, - { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, - { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, - { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, - { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, - { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, - { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, - { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, - { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, - { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, - { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, - { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, - { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, - { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, - { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, - { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, - { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, - { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, - { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, - { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, - { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, - { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, - { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, - { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, + { url = "https://files.pythonhosted.org/packages/67/41/5aa7fa3b0f4dc6b47dcafc3cea909299c37e40e9972feabc8b6a74e2730d/orjson-3.11.8-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:003646067cc48b7fcab2ae0c562491c9b5d2cbd43f1e5f16d98fd118c5522d34", size = 229229, upload-time = "2026-03-31T16:14:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d7/57e7f2458e0a2c41694f39fc830030a13053a84f837a5b73423dca1f0938/orjson-3.11.8-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ed193ce51d77a3830cad399a529cd4ef029968761f43ddc549e1bc62b40d88f8", size = 128871, upload-time = "2026-03-31T16:14:51.888Z" }, + { url = "https://files.pythonhosted.org/packages/53/4a/e0fdb9430983e6c46e0299559275025075568aad5d21dd606faee3703924/orjson-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8", size = 132104, upload-time = "2026-03-31T16:14:53.142Z" }, + { url = "https://files.pythonhosted.org/packages/08/4a/2025a60ff3f5c8522060cda46612d9b1efa653de66ed2908591d8d82f22d/orjson-3.11.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eda5b8b6be91d3f26efb7dc6e5e68ee805bc5617f65a328587b35255f138bf4", size = 130483, upload-time = "2026-03-31T16:14:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3c/b9cde05bdc7b2385c66014e0620627da638d3d04e4954416ab48c31196c5/orjson-3.11.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee8db7bfb6fe03581bbab54d7c4124a6dd6a7f4273a38f7267197890f094675f", size = 135481, upload-time = "2026-03-31T16:14:55.901Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f2/a8238e7734de7cb589fed319857a8025d509c89dc52fdcc88f39c6d03d5a/orjson-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d8b5231de76c528a46b57010bbd83fb51e056aa0220a372fd5065e978406f1c", size = 146819, upload-time = "2026-03-31T16:14:57.548Z" }, + { url = "https://files.pythonhosted.org/packages/db/10/dbf1e2a3cafea673b1b4350e371877b759060d6018a998643b7040e5de48/orjson-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58a4a208a6fbfdb7a7327b8f201c6014f189f721fd55d047cafc4157af1bc62a", size = 132846, upload-time = "2026-03-31T16:14:58.91Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fc/55e667ec9c85694038fcff00573d221b085d50777368ee3d77f38668bf3c/orjson-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f8952d6d2505c003e8f0224ff7858d341fa4e33fef82b91c4ff0ef070f2393c", size = 133580, upload-time = "2026-03-31T16:15:00.519Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a6/c08c589a9aad0cb46c4831d17de212a2b6901f9d976814321ff8e69e8785/orjson-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0022bb50f90da04b009ce32c512dc1885910daa7cb10b7b0cba4505b16db82a8", size = 142042, upload-time = "2026-03-31T16:15:01.906Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cc/2f78ea241d52b717d2efc38878615fe80425bf2beb6e68c984dde257a766/orjson-3.11.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6", size = 423845, upload-time = "2026-03-31T16:15:03.703Z" }, + { url = "https://files.pythonhosted.org/packages/70/07/c17dcf05dd8045457538428a983bf1f1127928df5bf328cb24d2b7cddacb/orjson-3.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6dbe9a97bdb4d8d9d5367b52a7c32549bba70b2739c58ef74a6964a6d05ae054", size = 147729, upload-time = "2026-03-31T16:15:05.203Z" }, + { url = "https://files.pythonhosted.org/packages/90/6c/0fb6e8a24e682e0958d71711ae6f39110e4b9cd8cab1357e2a89cb8e1951/orjson-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5c370674ebabe16c6ccac33ff80c62bf8a6e59439f5e9d40c1f5ab8fd2215b7", size = 136425, upload-time = "2026-03-31T16:15:07.052Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/4d3cc3a3d616035beb51b24a09bb872942dc452cf2df0c1d11ab35046d9f/orjson-3.11.8-cp311-cp311-win32.whl", hash = "sha256:0e32f7154299f42ae66f13488963269e5eccb8d588a65bc839ed986919fc9fac", size = 131870, upload-time = "2026-03-31T16:15:08.678Z" }, + { url = "https://files.pythonhosted.org/packages/13/26/9fe70f81d16b702f8c3a775e8731b50ad91d22dacd14c7599b60a0941cd1/orjson-3.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:25e0c672a2e32348d2eb33057b41e754091f2835f87222e4675b796b92264f06", size = 127440, upload-time = "2026-03-31T16:15:09.994Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c6/b038339f4145efd2859c1ca53097a52c0bb9cbdd24f947ebe146da1ad067/orjson-3.11.8-cp311-cp311-win_arm64.whl", hash = "sha256:9185589c1f2a944c17e26c9925dcdbc2df061cc4a145395c57f0c51f9b5dbfcd", size = 127399, upload-time = "2026-03-31T16:15:11.412Z" }, + { url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233, upload-time = "2026-03-31T16:15:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772, upload-time = "2026-03-31T16:15:14.237Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946, upload-time = "2026-03-31T16:15:15.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/da/4e/58b927e08fbe9840e6c920d9e299b051ea667463b1f39a56e668669f8508/orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec", size = 135540, upload-time = "2026-03-31T16:15:18.404Z" }, + { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/44/09/e12423d327071c851c13e76936f144a96adacfc037394dec35ac3fc8d1e8/orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942", size = 147806, upload-time = "2026-03-31T16:15:27.909Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" }, + { url = "https://files.pythonhosted.org/packages/be/c9/135194a02ab76b04ed9a10f68624b7ebd238bbe55548878b11ff15a0f352/orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2", size = 131966, upload-time = "2026-03-31T16:15:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/9796f8fbe3cf30ce9cb696748dbb535e5c87be4bf4fe2e9ca498ef1fa8cf/orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6", size = 127441, upload-time = "2026-03-31T16:15:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/cc/47/5aaf54524a7a4a0dd09dd778f3fa65dd2108290615b652e23d944152bc8e/orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d", size = 127364, upload-time = "2026-03-31T16:15:34.748Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/a6c55896197f97b6d4b4e7c7fd77e7235517c34f5d6ad5aadd43c54c6d7c/orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f", size = 135521, upload-time = "2026-03-31T16:15:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, + { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/ecfe62434096f8a794d4976728cb59bcfc4a643977f21c2040545d37eb4c/orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6", size = 147801, upload-time = "2026-03-31T16:15:52.939Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, + { url = "https://files.pythonhosted.org/packages/01/d6/6dde4f31842d87099238f1f07b459d24edc1a774d20687187443ab044191/orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d", size = 131956, upload-time = "2026-03-31T16:15:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f9/4e494a56e013db957fb77186b818b916d4695b8fa2aa612364974160e91b/orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8", size = 127410, upload-time = "2026-03-31T16:15:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/803203d00d6edb6e9e7eef421d4e1adbb5ea973e40b3533f3cfd9aeb374e/orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e", size = 127338, upload-time = "2026-03-31T16:15:59.106Z" }, + { url = "https://files.pythonhosted.org/packages/6d/35/b01910c3d6b85dc882442afe5060cbf719c7d1fc85749294beda23d17873/orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4", size = 229171, upload-time = "2026-03-31T16:16:00.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/56/c9ec97bd11240abef39b9e5d99a15462809c45f677420fd148a6c5e6295e/orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625", size = 128746, upload-time = "2026-03-31T16:16:02.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/66d4f30a90de45e2f0cbd9623588e8ae71eef7679dbe2ae954ed6d66a41f/orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5", size = 131867, upload-time = "2026-03-31T16:16:04.342Z" }, + { url = "https://files.pythonhosted.org/packages/19/30/2a645fc9286b928675e43fa2a3a16fb7b6764aa78cc719dc82141e00f30b/orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db", size = 124664, upload-time = "2026-03-31T16:16:05.837Z" }, + { url = "https://files.pythonhosted.org/packages/db/44/77b9a86d84a28d52ba3316d77737f6514e17118119ade3f91b639e859029/orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b", size = 129701, upload-time = "2026-03-31T16:16:07.407Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/eff3d9bfe47e9bc6969c9181c58d9f71237f923f9c86a2d2f490cd898c82/orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d", size = 141202, upload-time = "2026-03-31T16:16:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/90d4b4c60c84d62068d0cf9e4d8f0a4e05e76971d133ac0c60d818d4db20/orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858", size = 127194, upload-time = "2026-03-31T16:16:11.02Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c7/ea9e08d1f0ba981adffb629811148b44774d935171e7b3d780ae43c4c254/orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f", size = 133639, upload-time = "2026-03-31T16:16:13.434Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/ddbbfd6ba59453c8fc7fe1d0e5983895864e264c37481b2a791db635f046/orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d", size = 141914, upload-time = "2026-03-31T16:16:14.955Z" }, + { url = "https://files.pythonhosted.org/packages/4e/31/dbfbefec9df060d34ef4962cd0afcb6fa7a9ec65884cb78f04a7859526c3/orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc", size = 423800, upload-time = "2026-03-31T16:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/87/cf/f74e9ae9803d4ab46b163494adba636c6d7ea955af5cc23b8aaa94cfd528/orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf", size = 147837, upload-time = "2026-03-31T16:16:18.585Z" }, + { url = "https://files.pythonhosted.org/packages/64/e6/9214f017b5db85e84e68602792f742e5dc5249e963503d1b356bee611e01/orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600", size = 136441, upload-time = "2026-03-31T16:16:20.151Z" }, + { url = "https://files.pythonhosted.org/packages/24/dd/3590348818f58f837a75fb969b04cdf187ae197e14d60b5e5a794a38b79d/orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade", size = 131983, upload-time = "2026-03-31T16:16:21.823Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/b6cb692116e05d058f31ceee819c70f097fa9167c82f67fabe7516289abc/orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca", size = 127396, upload-time = "2026-03-31T16:16:23.685Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d1/facb5b5051fabb0ef9d26c6544d87ef19a939a9a001198655d0d891062dd/orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817", size = 127330, upload-time = "2026-03-31T16:16:25.496Z" }, ] [[package]] @@ -1856,98 +1883,98 @@ wheels = [ [[package]] name = "pathspec" -version = "0.12.1" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] name = "pillow" -version = "12.1.1" +version = "12.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, - { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, - { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, - { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, - { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, - { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, - { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, - { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, - { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, - { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, - { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, - { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, - { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, - { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, - { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, - { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, - { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, - { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, - { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, - { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, - { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, - { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, - { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, - { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, - { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, - { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, - { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, - { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, - { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, - { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, - { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, - { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, - { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, - { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, - { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, - { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, - { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, - { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, - { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, - { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, - { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, - { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, - { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, - { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, - { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, - { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, - { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, ] [[package]] @@ -2149,16 +2176,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.12.0" +version = "2.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, ] [[package]] @@ -2181,7 +2208,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2190,9 +2217,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -2210,16 +2237,16 @@ wheels = [ [[package]] name = "pytest-cov" -version = "7.0.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] @@ -2269,11 +2296,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.22" +version = "0.0.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, ] [[package]] @@ -2424,7 +2451,7 @@ wheels = [ [[package]] name = "rapidocr" -version = "3.6.0" +version = "3.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorlog" }, @@ -2440,7 +2467,7 @@ dependencies = [ { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/fd/0d025466f0f84552634f2a94c018df34568fe55cc97184a6bb2c719c5b3a/rapidocr-3.6.0-py3-none-any.whl", hash = "sha256:d16b43872fc4dfa1e60996334dcd0dc3e3f1f64161e2332bc1873b9f65754e6b", size = 15067340, upload-time = "2026-01-28T14:45:04.271Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4a/fa521d947f0fc7bb304bf11bec4cb66266bd81494588b4cb48dc01001719/rapidocr-3.8.1-py3-none-any.whl", hash = "sha256:650044b1fbce9e6bae5cae462dcf8be754cde11e2f23fc51f65dcc08deae2c46", size = 15080319, upload-time = "2026-04-11T07:13:22.56Z" }, ] [[package]] @@ -2460,15 +2487,15 @@ wheels = [ [[package]] name = "rich" -version = "14.3.2" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] @@ -2534,27 +2561,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.0" +version = "0.15.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, - { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, - { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, - { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, - { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, - { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, - { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, - { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, - { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, - { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, + { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, + { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, + { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, + { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, + { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, + { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, + { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, + { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, + { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, + { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, + { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, ] [[package]] @@ -2907,41 +2934,41 @@ wheels = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20250915" +version = "6.0.12.20260408" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/73/b759b1e413c31034cc01ecdfb96b38115d0ab4db55a752a3929f0cd449fd/types_pyyaml-6.0.12.20260408.tar.gz", hash = "sha256:92a73f2b8d7f39ef392a38131f76b970f8c66e4c42b3125ae872b7c93b556307", size = 17735, upload-time = "2026-04-08T04:30:50.974Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f0/c391068b86abb708882c6d75a08cd7d25b2c7227dab527b3a3685a3c635b/types_pyyaml-6.0.12.20260408-py3-none-any.whl", hash = "sha256:fbc42037d12159d9c801ebfcc79ebd28335a7c13b08a4cfbc6916df78fee9384", size = 20339, upload-time = "2026-04-08T04:30:50.113Z" }, ] [[package]] name = "types-requests" -version = "2.32.4.20260107" +version = "2.33.0.20260408" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/6a/749dc53a54a3f35842c1f8197b3ca6b54af6d7458a1bfc75f6629b6da666/types_requests-2.33.0.20260408.tar.gz", hash = "sha256:95b9a86376807a216b2fb412b47617b202091c3ea7c078f47cc358d5528ccb7b", size = 23882, upload-time = "2026-04-08T04:34:49.33Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, + { url = "https://files.pythonhosted.org/packages/90/b8/78fd6c037de4788c040fdd323b3369804400351b7827473920f6c1d03c10/types_requests-2.33.0.20260408-py3-none-any.whl", hash = "sha256:81f31d5ea4acb39f03be7bc8bed569ba6d5a9c5d97e89f45ac43d819b68ca50f", size = 20739, upload-time = "2026-04-08T04:34:48.325Z" }, ] [[package]] name = "types-setuptools" -version = "82.0.0.20260210" +version = "82.0.0.20260408" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/90/796ac8c774a7f535084aacbaa6b7053d16fff5c630eff87c3ecff7896c37/types_setuptools-82.0.0.20260210.tar.gz", hash = "sha256:d9719fbbeb185254480ade1f25327c4654f8c00efda3fec36823379cebcdee58", size = 44768, upload-time = "2026-02-10T04:22:02.107Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/12/3464b410c50420dd4674fa5fe9d3880711c1dbe1a06f5fe4960ee9067b9e/types_setuptools-82.0.0.20260408.tar.gz", hash = "sha256:036c68caf7e672a699f5ebbf914708d40644c14e05298bc49f7272be91cf43d3", size = 44861, upload-time = "2026-04-08T04:29:33.292Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/54/3489432b1d9bc713c9d8aa810296b8f5b0088403662959fb63a8acdbd4fc/types_setuptools-82.0.0.20260210-py3-none-any.whl", hash = "sha256:5124a7daf67f195c6054e0f00f1d97c69caad12fdcf9113eba33eff0bce8cd2b", size = 68433, upload-time = "2026-02-10T04:22:00.876Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e1/46a4fc3ef03aabf5d18bac9df5cf37c6b02c3bddf3e05c3533f4b4588331/types_setuptools-82.0.0.20260408-py3-none-any.whl", hash = "sha256:ece0a215cdfa6463a65fd6f68bd940f39e455729300ddfe61cab1147ed1d2462", size = 68428, upload-time = "2026-04-08T04:29:32.175Z" }, ] [[package]] name = "types-simplejson" -version = "3.20.0.20250822" +version = "3.20.0.20260408" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/6b/96d43a90cd202bd552cdd871858a11c138fe5ef11aeb4ed8e8dc51389257/types_simplejson-3.20.0.20250822.tar.gz", hash = "sha256:2b0bfd57a6beed3b932fd2c3c7f8e2f48a7df3978c9bba43023a32b3741a95b0", size = 10608, upload-time = "2025-08-22T03:03:35.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/36/e319fd0f6d906dbf7c2c03eef17db77ef461197a75b253fccd9c7c695d3e/types_simplejson-3.20.0.20260408.tar.gz", hash = "sha256:0b0e1bf61e70f81dfe6ef4c2b9c02e39403848c0652df334e7a430c3a26c06b3", size = 10693, upload-time = "2026-04-08T04:28:07.8Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/9f/8e2c9e6aee9a2ff34f2ffce6ccd9c26edeef6dfd366fde611dc2e2c00ab9/types_simplejson-3.20.0.20250822-py3-none-any.whl", hash = "sha256:b5e63ae220ac7a1b0bb9af43b9cb8652237c947981b2708b0c776d3b5d8fa169", size = 10417, upload-time = "2025-08-22T03:03:34.485Z" }, + { url = "https://files.pythonhosted.org/packages/22/c0/01a5a4c3948c2269cf9d727e5e66a8b404e03beb4f9522680a3f71097011/types_simplejson-3.20.0.20260408-py3-none-any.whl", hash = "sha256:f9e542199cb159ed34ad54b6ceb3dc9af890c256b810ad1bd7c69c61db7d2236", size = 10415, upload-time = "2026-04-08T04:28:06.984Z" }, ] [[package]] @@ -2985,15 +3012,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.40.0" +version = "0.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, ] [package.optional-dependencies] diff --git a/mise.toml b/mise.toml index 0ec32de20c..c4700fd924 100644 --- a/mise.toml +++ b/mise.toml @@ -14,10 +14,10 @@ config_roots = [ ] [tools] -node = "24.13.1" -flutter = "3.35.7" -pnpm = "10.30.3" -terragrunt = "0.99.4" +node = "24.14.1" +flutter = "3.41.6" +pnpm = "10.33.0" +terragrunt = "1.0.0" opentofu = "1.11.5" java = "21.0.2" diff --git a/mobile/.isar b/mobile/.isar deleted file mode 160000 index 6643d064ab..0000000000 --- a/mobile/.isar +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6643d064abf22606b6c6a741ea873e4781115ef4 diff --git a/mobile/.isar-cargo.lock b/mobile/.isar-cargo.lock deleted file mode 100644 index a7b1dd37b9..0000000000 --- a/mobile/.isar-cargo.lock +++ /dev/null @@ -1,859 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "autocfg" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - -[[package]] -name = "bindgen" -version = "0.63.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885" -dependencies = [ - "bitflags 1.3.2", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 1.0.109", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" - -[[package]] -name = "cc" -version = "1.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d74707dde2ba56f86ae90effb3b43ddd369504387e718014de010cec7959800" -dependencies = [ - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "cmake" -version = "0.1.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" -dependencies = [ - "cc", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" - -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - -[[package]] -name = "enum_dispatch" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.77", -] - -[[package]] -name = "float_next_after" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc612c5837986b7104a87a0df74a5460931f1c5274be12f8d0f40aa2f30d632" -dependencies = [ - "num-traits", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "intmap" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee87fd093563344074bacf24faa0bb0227fb6969fb223e922db798516de924d6" - -[[package]] -name = "isar" -version = "0.0.0" -dependencies = [ - "dirs", - "intmap", - "isar-core", - "itertools", - "jni", - "ndk-context", - "objc", - "objc-foundation", - "once_cell", - "paste", - "serde_json", - "threadpool", - "unicode-segmentation", -] - -[[package]] -name = "isar-core" -version = "0.0.0" -dependencies = [ - "byteorder", - "cfg-if", - "crossbeam-channel", - "enum_dispatch", - "float_next_after", - "intmap", - "itertools", - "libc", - "mdbx-sys", - "once_cell", - "paste", - "rand", - "serde", - "serde_json", - "snafu", - "widestring", - "xxhash-rust", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" - -[[package]] -name = "jni" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "libc" -version = "0.2.158" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" - -[[package]] -name = "libloading" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" -dependencies = [ - "cfg-if", - "windows-targets", -] - -[[package]] -name = "libredox" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" -dependencies = [ - "bitflags 2.6.0", - "libc", -] - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "mdbx-sys" -version = "0.0.0" -dependencies = [ - "bindgen", - "cc", - "cmake", - "libc", -] - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - -[[package]] -name = "once_cell" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ea5043e58958ee56f3e15a90aee535795cd7dfd319846288d93c5b57d85cbe" - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - -[[package]] -name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom", - "libredox", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "serde" -version = "1.0.210" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.210" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - -[[package]] -name = "serde_json" -version = "1.0.128" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "snafu" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" -dependencies = [ - "doc-comment", - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] -name = "unicode-ident" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "widestring" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "xxhash-rust" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984" - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json index eafbef8102..051c18ce6a 100644 --- a/mobile/.vscode/settings.json +++ b/mobile/.vscode/settings.json @@ -1,5 +1,5 @@ { - "dart.flutterSdkPath": ".fvm/versions/3.35.7", + "dart.flutterSdkPath": ".fvm/versions/3.41.7", "dart.lineLength": 120, "[dart]": { "editor.rulers": [ diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 895203fb98..fafd1f40ec 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -52,90 +52,11 @@ analyzer: unawaited_futures: warning custom_lint: - debug: true rules: - avoid_build_context_in_providers: false - avoid_public_notifier_properties: false - avoid_manual_providers_as_generated_provider_dependency: false - unsupported_provider_value: false - - import_rule_photo_manager: - message: photo_manager must only be used in MediaRepositories - restrict: package:photo_manager - allowed: - # required / wanted - - 'lib/infrastructure/repositories/album_media.repository.dart' - - 'lib/infrastructure/repositories/{storage,asset_media}.repository.dart' - - 'lib/repositories/{album,asset,file}_media.repository.dart' - # acceptable exceptions for the time being - - lib/entities/asset.entity.dart # to provide local AssetEntity for now - - lib/providers/image/immich_local_{image,thumbnail}_provider.dart # accesses thumbnails via PhotoManager - # refactor to make the providers and services testable - - lib/providers/backup/{backup,manual_upload}.provider.dart # uses only PMProgressHandler - - lib/services/{background,backup}.service.dart # uses only PMProgressHandler - - test/**.dart - - import_rule_isar: - message: isar must only be used in entities and repositories - restrict: package:isar - allowed: - # required / wanted - - lib/entities/*.entity.dart - - lib/repositories/{album,asset,backup,database,etag,exif_info,user,timeline,partner}.repository.dart - - lib/infrastructure/entities/*.entity.dart - - lib/infrastructure/repositories/*.repository.dart - - lib/providers/infrastructure/db.provider.dart - # acceptable exceptions for the time being (until Isar is fully replaced) - - lib/providers/app_life_cycle.provider.dart - - integration_test/test_utils/general_helper.dart - - lib/domain/services/background_worker.service.dart - - lib/main.dart - - lib/pages/album/album_asset_selection.page.dart - - lib/routing/router.dart - - lib/services/immich_logger.service.dart # not really a service... more a util - - lib/utils/{db,migration}.dart - - lib/utils/bootstrap.dart - - lib/widgets/asset_grid/asset_grid_data_structure.dart - - test/**.dart - # refactor the remaining providers - - lib/providers/db.provider.dart - - - import_rule_openapi: - message: openapi must only be used through ApiRepositories - restrict: package:openapi - allowed: - # required / wanted - - lib/repositories/*_api.repository.dart - - lib/domain/models/sync_event.model.dart - - lib/{domain,infrastructure}/**/sync_stream.* - - lib/{domain,infrastructure}/**/sync_api.* - - lib/infrastructure/repositories/*_api.repository.dart - - lib/infrastructure/utils/*.converter.dart - # acceptable exceptions for the time being - - lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities - - lib/infrastructure/utils/*.converter.dart - - lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine - - test/modules/utils/openapi_patching_test.dart # filename is self-explanatory... - - lib/domain/services/sync_stream.service.dart # Making sure to comply with the type from database - - lib/domain/services/search.service.dart - - # refactor - - lib/models/map/map_marker.model.dart - - lib/models/server_info/server_{config,disk_info,features,version}.model.dart - - lib/models/shared_link/shared_link.model.dart - - lib/providers/asset_viewer/asset_people.provider.dart - - lib/providers/auth.provider.dart - - lib/providers/image/immich_remote_{image,thumbnail}_provider.dart - - lib/providers/map/map_state.provider.dart - - lib/providers/search/{search,search_filter}.provider.dart - - lib/providers/websocket.provider.dart - - lib/routing/auth_guard.dart - - lib/services/{api,asset,backup,memory,oauth,search,shared_link,stack,trash}.service.dart - - lib/widgets/album/album_thumbnail_listtile.dart - - lib/widgets/forms/login/login_form.dart - - lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart - - lib/services/auth.service.dart # on ApiException - - test/services/auth.service_test.dart # on ApiException - # allow import from test - - test/**.dart dart_code_metrics: rules: diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 103cf79e4e..e879b54ae5 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -1,27 +1,10 @@ plugins { - id "com.android.application" - id "kotlin-android" + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) id "dev.flutter.flutter-gradle-plugin" - id 'com.google.devtools.ksp' - id 'org.jetbrains.kotlin.plugin.serialization' - id 'org.jetbrains.kotlin.plugin.compose' version '2.0.20' // this version matches your Kotlin version - -} - -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withInputStream { localProperties.load(it) } -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' + alias(libs.plugins.ksp) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.compose) } def keystoreProperties = new Properties() @@ -31,8 +14,8 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 35 - ndkVersion = "28.2.13676358" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_17 @@ -55,10 +38,10 @@ android { defaultConfig { applicationId "app.alextran.immich" - minSdkVersion 26 - targetSdkVersion 35 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName + minSdk = 26 + targetSdk = flutter.targetSdkVersion + versionCode flutter.versionCode + versionName flutter.versionName } signingConfigs { @@ -67,10 +50,10 @@ android { def keyPasswordVal = System.getenv("ANDROID_KEY_PASSWORD") def storePasswordVal = System.getenv("ANDROID_STORE_PASSWORD") - keyAlias keyAliasVal ? keyAliasVal : keystoreProperties['keyAlias'] - keyPassword keyPasswordVal ? keyPasswordVal : keystoreProperties['keyPassword'] - storeFile file("../key.jks") ? file("../key.jks") : file(keystoreProperties['storeFile']) - storePassword storePasswordVal ? storePasswordVal : keystoreProperties['storePassword'] + keyAlias keyAliasVal ?: keystoreProperties['keyAlias'] + keyPassword keyPasswordVal ?: keystoreProperties['keyPassword'] + storeFile file("../key.jks").exists() ? file("../key.jks") : file(keystoreProperties['storeFile'] ?: '../key.jks') + storePassword storePasswordVal ?: keystoreProperties['storePassword'] } } @@ -99,43 +82,31 @@ flutter { } dependencies { - def kotlin_version = '2.0.20' - def kotlin_coroutines_version = '1.9.0' - def work_version = '2.9.1' - def concurrent_version = '1.2.0' - def guava_version = '33.3.1-android' - def glide_version = '4.16.0' - def serialization_version = '1.8.1' - def compose_version = '1.1.1' - def gson_version = '2.10.1' - def okhttp_version = '4.12.0' + implementation libs.okhttp + implementation libs.cronet.embedded + implementation libs.media3.datasource.okhttp + implementation libs.media3.datasource.cronet + implementation libs.kotlinx.coroutines.android + implementation libs.work.runtime.ktx + implementation libs.concurrent.futures + implementation libs.guava + implementation libs.glide + implementation libs.kotlinx.serialization.json - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation "com.squareup.okhttp3:okhttp:$okhttp_version" - implementation 'org.chromium.net:cronet-embedded:143.7445.0' - implementation("androidx.media3:media3-datasource-okhttp:1.9.2") - implementation("androidx.media3:media3-datasource-cronet:1.9.2") - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" - implementation "androidx.work:work-runtime-ktx:$work_version" - implementation "androidx.concurrent:concurrent-futures:$concurrent_version" - implementation "com.google.guava:guava:$guava_version" - implementation "com.github.bumptech.glide:glide:$glide_version" - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version" - - ksp "com.github.bumptech.glide:ksp:$glide_version" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' + ksp libs.glide.ksp + coreLibraryDesugaring libs.desugar.jdk.libs //Glance Widget - implementation "androidx.glance:glance-appwidget:$compose_version" - implementation "com.google.code.gson:gson:$gson_version" + implementation libs.glance.appwidget + implementation libs.gson // Glance Configure - implementation "androidx.activity:activity-compose:1.8.2" - implementation "androidx.compose.ui:ui:$compose_version" - implementation "androidx.compose.ui:ui-tooling:$compose_version" - implementation "androidx.compose.material3:material3:1.2.1" - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2" - implementation "com.google.android.material:material:1.12.0" + implementation libs.activity.compose + implementation libs.compose.ui + implementation libs.compose.ui.tooling + implementation libs.compose.material3 + implementation libs.lifecycle.runtime.ktx + implementation libs.material } // This is uncommented in F-Droid build script diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index db3859ab6e..436d8c492d 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -39,10 +39,6 @@ android:exported="false" android:foregroundServiceType="dataSync|shortService" /> - - diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt deleted file mode 100644 index f62f25558d..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt +++ /dev/null @@ -1,389 +0,0 @@ -package app.alextran.immich - -import android.app.Activity -import android.content.ContentResolver -import android.content.ContentUris -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.provider.MediaStore -import android.provider.Settings -import android.util.Log -import androidx.annotation.RequiresApi -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry -import java.security.MessageDigest -import java.io.FileInputStream -import kotlinx.coroutines.* -import androidx.core.net.toUri - -/** - * Android plugin for Dart `BackgroundService` and file trash operations - */ -class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener { - - private var methodChannel: MethodChannel? = null - private var fileTrashChannel: MethodChannel? = null - private var context: Context? = null - private var pendingResult: Result? = null - private val permissionRequestCode = 1001 - private val trashRequestCode = 1002 - private var activityBinding: ActivityPluginBinding? = null - - override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onAttachedToEngine(binding.applicationContext, binding.binaryMessenger) - } - - private fun onAttachedToEngine(ctx: Context, messenger: BinaryMessenger) { - context = ctx - methodChannel = MethodChannel(messenger, "immich/foregroundChannel") - methodChannel?.setMethodCallHandler(this) - - // Add file trash channel - fileTrashChannel = MethodChannel(messenger, "file_trash") - fileTrashChannel?.setMethodCallHandler(this) - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onDetachedFromEngine() - } - - private fun onDetachedFromEngine() { - methodChannel?.setMethodCallHandler(null) - methodChannel = null - fileTrashChannel?.setMethodCallHandler(null) - fileTrashChannel = null - } - - override fun onMethodCall(call: MethodCall, result: Result) { - val ctx = context!! - when (call.method) { - // Existing BackgroundService methods - "enable" -> { - val args = call.arguments>()!! - ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit() - .putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true) - .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args[0] as Long) - .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args[1] as String) - .apply() - ContentObserverWorker.enable(ctx, immediate = args[2] as Boolean) - result.success(true) - } - - "configure" -> { - val args = call.arguments>()!! - val requireUnmeteredNetwork = args[0] as Boolean - val requireCharging = args[1] as Boolean - val triggerUpdateDelay = (args[2] as Number).toLong() - val triggerMaxDelay = (args[3] as Number).toLong() - ContentObserverWorker.configureWork( - ctx, - requireUnmeteredNetwork, - requireCharging, - triggerUpdateDelay, - triggerMaxDelay - ) - result.success(true) - } - - "disable" -> { - ContentObserverWorker.disable(ctx) - BackupWorker.stopWork(ctx) - result.success(true) - } - - "isEnabled" -> { - result.success(ContentObserverWorker.isEnabled(ctx)) - } - - "isIgnoringBatteryOptimizations" -> { - result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx)) - } - - "digestFiles" -> { - val args = call.arguments>()!! - GlobalScope.launch(Dispatchers.IO) { - val buf = ByteArray(BUFFER_SIZE) - val digest: MessageDigest = MessageDigest.getInstance("SHA-1") - val hashes = arrayOfNulls(args.size) - for (i in args.indices) { - val path = args[i] - var len = 0 - try { - val file = FileInputStream(path) - file.use { assetFile -> - while (true) { - len = assetFile.read(buf) - if (len != BUFFER_SIZE) break - digest.update(buf) - } - } - digest.update(buf, 0, len) - hashes[i] = digest.digest() - } catch (e: Exception) { - // skip this file - Log.w(TAG, "Failed to hash file ${args[i]}: $e") - } - } - result.success(hashes.asList()) - } - } - - // File Trash methods moved from MainActivity - "moveToTrash" -> { - val mediaUrls = call.argument>("mediaUrls") - if (mediaUrls != null) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { - moveToTrash(mediaUrls, result) - } else { - result.error("PERMISSION_DENIED", "Media permission required", null) - } - } else { - result.error("INVALID_NAME", "The mediaUrls is not specified.", null) - } - } - - "restoreFromTrash" -> { - val fileName = call.argument("fileName") - val type = call.argument("type") - val mediaId = call.argument("mediaId") - if (fileName != null && type != null) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { - restoreFromTrash(fileName, type, result) - } else { - result.error("PERMISSION_DENIED", "Media permission required", null) - } - } else - if (mediaId != null && type != null) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { - restoreFromTrashById(mediaId, type, result) - } else { - result.error("PERMISSION_DENIED", "Media permission required", null) - } - } else { - result.error("INVALID_PARAMS", "Required params are not specified.", null) - } - } - - "requestManageMediaPermission" -> { - if (!hasManageMediaPermission()) { - requestManageMediaPermission(result) - } else { - Log.e("Manage storage permission", "Permission already granted") - result.success(true) - } - } - - "hasManageMediaPermission" -> { - if (hasManageMediaPermission()) { - Log.i("Manage storage permission", "Permission already granted") - result.success(true) - } else { - result.success(false) - } - } - - "manageMediaPermission" -> requestManageMediaPermission(result) - - else -> result.notImplemented() - } - } - - private fun hasManageMediaPermission(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - MediaStore.canManageMedia(context!!); - } else { - false - } - } - - private fun requestManageMediaPermission(result: Result) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - pendingResult = result // Store the result callback - val activity = activityBinding?.activity ?: return - - val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA) - intent.data = "package:${activity.packageName}".toUri() - activity.startActivityForResult(intent, permissionRequestCode) - } else { - result.success(false) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun moveToTrash(mediaUrls: List, result: Result) { - val urisToTrash = mediaUrls.map { it.toUri() } - if (urisToTrash.isEmpty()) { - result.error("INVALID_ARGS", "No valid URIs provided", null) - return - } - - toggleTrash(urisToTrash, true, result); - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun restoreFromTrash(name: String, type: Int, result: Result) { - val uri = getTrashedFileUri(name, type) - if (uri == null) { - Log.e("TrashError", "Asset Uri cannot be found obtained") - result.error("TrashError", "Asset Uri cannot be found obtained", null) - return - } - Log.e("FILE_URI", uri.toString()) - uri.let { toggleTrash(listOf(it), false, result) } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun restoreFromTrashById(mediaId: String, type: Int, result: Result) { - val id = mediaId.toLongOrNull() - if (id == null) { - result.error("INVALID_ID", "The file id is not a valid number: $mediaId", null) - return - } - if (!isInTrash(id)) { - result.error("TrashNotFound", "Item with id=$id not found in trash", null) - return - } - - val uri = ContentUris.withAppendedId(contentUriForType(type), id) - - try { - Log.i(TAG, "restoreFromTrashById: uri=$uri (type=$type,id=$id)") - restoreUris(listOf(uri), result) - } catch (e: Exception) { - Log.w(TAG, "restoreFromTrashById failed", e) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun toggleTrash(contentUris: List, isTrashed: Boolean, result: Result) { - val activity = activityBinding?.activity - val contentResolver = context?.contentResolver - if (activity == null || contentResolver == null) { - result.error("TrashError", "Activity or ContentResolver not available", null) - return - } - - try { - val pendingIntent = MediaStore.createTrashRequest(contentResolver, contentUris, isTrashed) - pendingResult = result // Store for onActivityResult - activity.startIntentSenderForResult( - pendingIntent.intentSender, - trashRequestCode, - null, 0, 0, 0 - ) - } catch (e: Exception) { - Log.e("TrashError", "Error creating or starting trash request", e) - result.error("TrashError", "Error creating or starting trash request", null) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun getTrashedFileUri(fileName: String, type: Int): Uri? { - val contentResolver = context?.contentResolver ?: return null - val queryUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - val projection = arrayOf(MediaStore.Files.FileColumns._ID) - - val queryArgs = Bundle().apply { - putString( - ContentResolver.QUERY_ARG_SQL_SELECTION, - "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?" - ) - putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName)) - putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY) - } - - contentResolver.query(queryUri, projection, queryArgs, null)?.use { cursor -> - if (cursor.moveToFirst()) { - val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)) - return ContentUris.withAppendedId(contentUriForType(type), id) - } - } - return null - } - - // ActivityAware implementation - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activityBinding = binding - binding.addActivityResultListener(this) - } - - override fun onDetachedFromActivityForConfigChanges() { - activityBinding?.removeActivityResultListener(this) - activityBinding = null - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - activityBinding = binding - binding.addActivityResultListener(this) - } - - override fun onDetachedFromActivity() { - activityBinding?.removeActivityResultListener(this) - activityBinding = null - } - - // ActivityResultListener implementation - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - if (requestCode == permissionRequestCode) { - val granted = hasManageMediaPermission() - pendingResult?.success(granted) - pendingResult = null - return true - } - - if (requestCode == trashRequestCode) { - val approved = resultCode == Activity.RESULT_OK - pendingResult?.success(approved) - pendingResult = null - return true - } - return false - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun isInTrash(id: Long): Boolean { - val contentResolver = context?.contentResolver ?: return false - val filesUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - val args = Bundle().apply { - putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns._ID}=?") - putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(id.toString())) - putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY) - putInt(ContentResolver.QUERY_ARG_LIMIT, 1) - } - return contentResolver.query(filesUri, arrayOf(MediaStore.Files.FileColumns._ID), args, null) - ?.use { it.moveToFirst() } == true - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun restoreUris(uris: List, result: Result) { - if (uris.isEmpty()) { - result.error("TrashError", "No URIs to restore", null) - return - } - Log.i(TAG, "restoreUris: count=${uris.size}, first=${uris.first()}") - toggleTrash(uris, false, result) - } - - @RequiresApi(Build.VERSION_CODES.Q) - private fun contentUriForType(type: Int): Uri = - when (type) { - // same order as AssetType from dart - 1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI - 2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI - 3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - else -> MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - } -} - -private const val TAG = "BackgroundServicePlugin" -private const val BUFFER_SIZE = 2 * 1024 * 1024 diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt deleted file mode 100644 index 9c90528dc9..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt +++ /dev/null @@ -1,394 +0,0 @@ -package app.alextran.immich - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE -import android.os.Build -import android.os.Handler -import android.os.Looper -import android.os.PowerManager -import android.os.SystemClock -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat -import androidx.concurrent.futures.ResolvableFuture -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.ForegroundInfo -import androidx.work.ListenableWorker -import androidx.work.NetworkType -import androidx.work.WorkerParameters -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import androidx.work.WorkInfo -import com.google.common.util.concurrent.ListenableFuture -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.embedding.engine.dart.DartExecutor -import io.flutter.embedding.engine.loader.FlutterLoader -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.view.FlutterCallbackInformation -import java.util.concurrent.TimeUnit - -/** - * Worker executed by Android WorkManager to perform backup in background - * - * Starts the Dart runtime/engine and calls `_nativeEntry` function in - * `background.service.dart` to run the actual backup logic. - * Called by Android WorkManager when all constraints for the work are met, - * i.e. battery is not low and optionally Wifi and charging are active. - */ -class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), - MethodChannel.MethodCallHandler { - - private val resolvableFuture = ResolvableFuture.create() - private var engine: FlutterEngine? = null - private lateinit var backgroundChannel: MethodChannel - private val notificationManager = - ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext) - private var timeBackupStarted: Long = 0L - private var notificationBuilder: NotificationCompat.Builder? = null - private var notificationDetailBuilder: NotificationCompat.Builder? = null - private var fgFuture: ListenableFuture? = null - - override fun startWork(): ListenableFuture { - - Log.d(TAG, "startWork") - - val ctx = applicationContext - - if (!flutterLoader.initialized()) { - flutterLoader.startInitialization(ctx) - } - - // Create a Notification channel - createChannel() - - Log.d(TAG, "isIgnoringBatteryOptimizations $isIgnoringBatteryOptimizations") - if (isIgnoringBatteryOptimizations) { - // normal background services can only up to 10 minutes - // foreground services are allowed to run indefinitely - // requires battery optimizations to be disabled (either manually by the user - // or by the system learning that immich is important to the user) - val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!! - showInfo(getInfoBuilder(title, indeterminate = true).build()) - } - - engine = FlutterEngine(ctx) - - flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { - runDart() - - } - - return resolvableFuture - } - - /** - * Starts the Dart runtime/engine and calls `_nativeEntry` function in - * `background.service.dart` to run the actual backup logic. - */ - private fun runDart() { - val callbackDispatcherHandle = applicationContext.getSharedPreferences( - SHARED_PREF_NAME, Context.MODE_PRIVATE - ).getLong(SHARED_PREF_CALLBACK_KEY, 0L) - val callbackInformation = - FlutterCallbackInformation.lookupCallbackInformation(callbackDispatcherHandle) - val appBundlePath = flutterLoader.findAppBundlePath() - - engine?.let { engine -> - backgroundChannel = MethodChannel(engine.dartExecutor, "immich/backgroundChannel") - backgroundChannel.setMethodCallHandler(this@BackupWorker) - engine.dartExecutor.executeDartCallback( - DartExecutor.DartCallback( - applicationContext.assets, - appBundlePath, - callbackInformation - ) - ) - } - } - - override fun onStopped() { - Log.d(TAG, "onStopped") - // called when the system has to stop this worker because constraints are - // no longer met or the system needs resources for more important tasks - Handler(Looper.getMainLooper()).postAtFrontOfQueue { - if (::backgroundChannel.isInitialized) { - backgroundChannel.invokeMethod("systemStop", null) - } - } - waitOnSetForegroundAsync() - // cannot await/get(block) on resolvableFuture as its already cancelled (would throw CancellationException) - // instead, wait for 5 seconds until forcefully stopping backup work - Handler(Looper.getMainLooper()).postDelayed({ - stopEngine(null) - }, 5000) - } - - private fun waitOnSetForegroundAsync() { - val fgFuture = this.fgFuture - if (fgFuture != null && !fgFuture.isCancelled && !fgFuture.isDone) { - try { - fgFuture.get(500, TimeUnit.MILLISECONDS) - } catch (e: Exception) { - // ignored, there is nothing to be done - } - } - } - - private fun stopEngine(result: Result?) { - clearBackgroundNotification() - engine?.destroy() - engine = null - if (result != null) { - Log.d(TAG, "stopEngine result=${result}") - resolvableFuture.set(result) - } - waitOnSetForegroundAsync() - } - - @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) { - when (call.method) { - "initialized" -> { - timeBackupStarted = SystemClock.uptimeMillis() - backgroundChannel.invokeMethod( - "onAssetsChanged", - null, - object : MethodChannel.Result { - override fun notImplemented() { - stopEngine(Result.failure()) - } - - override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { - stopEngine(Result.failure()) - } - - override fun success(receivedResult: Any?) { - val success = receivedResult as Boolean - stopEngine(if (success) Result.success() else Result.retry()) - } - } - ) - } - - "updateNotification" -> { - val args = call.arguments>()!! - val title = args[0] as String? - val content = args[1] as String? - val progress = args[2] as Int - val max = args[3] as Int - val indeterminate = args[4] as Boolean - val isDetail = args[5] as Boolean - val onlyIfFG = args[6] as Boolean - if (!onlyIfFG || isIgnoringBatteryOptimizations) { - showInfo( - getInfoBuilder(title, content, isDetail, progress, max, indeterminate).build(), - isDetail - ) - } - } - - "showError" -> { - val args = call.arguments>()!! - val title = args[0] as String - val content = args[1] as String? - val individualTag = args[2] as String? - showError(title, content, individualTag) - } - - "clearErrorNotifications" -> clearErrorNotifications() - "hasContentChanged" -> { - val lastChange = applicationContext - .getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getLong(SHARED_PREF_LAST_CHANGE, timeBackupStarted) - val hasContentChanged = lastChange > timeBackupStarted; - timeBackupStarted = SystemClock.uptimeMillis() - r.success(hasContentChanged) - } - - else -> r.notImplemented() - } - } - - private fun showError(title: String, content: String?, individualTag: String?) { - val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID) - .setContentTitle(title) - .setTicker(title) - .setContentText(content) - .setSmallIcon(R.drawable.notification_icon) - .build() - notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification) - } - - private fun clearErrorNotifications() { - notificationManager.cancel(NOTIFICATION_ERROR_ID) - } - - private fun clearBackgroundNotification() { - notificationManager.cancel(NOTIFICATION_ID) - notificationManager.cancel(NOTIFICATION_DETAIL_ID) - } - - private fun showInfo(notification: Notification, isDetail: Boolean = false) { - val id = if (isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID - - if (isIgnoringBatteryOptimizations && !isDetail) { - fgFuture = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE)) - } else { - setForegroundAsync(ForegroundInfo(id, notification)) - } - } else { - notificationManager.notify(id, notification) - } - } - - private fun getInfoBuilder( - title: String? = null, - content: String? = null, - isDetail: Boolean = false, - progress: Int = 0, - max: Int = 0, - indeterminate: Boolean = false, - ): NotificationCompat.Builder { - var builder = if (isDetail) notificationDetailBuilder else notificationBuilder - if (builder == null) { - builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID) - .setSmallIcon(R.drawable.notification_icon) - .setOnlyAlertOnce(true) - .setOngoing(true) - if (isDetail) { - notificationDetailBuilder = builder - } else { - notificationBuilder = builder - } - } - if (title != null) { - builder.setTicker(title).setContentTitle(title) - } - if (content != null) { - builder.setContentText(content) - } - return builder.setProgress(max, progress, indeterminate) - } - - private fun createChannel() { - val foreground = NotificationChannel( - NOTIFICATION_CHANNEL_ID, - NOTIFICATION_CHANNEL_ID, - NotificationManager.IMPORTANCE_LOW - ) - notificationManager.createNotificationChannel(foreground) - val error = NotificationChannel( - NOTIFICATION_CHANNEL_ERROR_ID, - NOTIFICATION_CHANNEL_ERROR_ID, - NotificationManager.IMPORTANCE_HIGH - ) - notificationManager.createNotificationChannel(error) - } - - companion object { - const val SHARED_PREF_NAME = "immichBackgroundService" - const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle" - const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle" - const val SHARED_PREF_LAST_CHANGE = "lastChange" - - private const val TASK_NAME_BACKUP = "immich/BackupWorker" - private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService" - private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError" - private const val NOTIFICATION_DEFAULT_TITLE = "Immich" - private const val NOTIFICATION_ID = 1 - private const val NOTIFICATION_ERROR_ID = 2 - private const val NOTIFICATION_DETAIL_ID = 3 - private const val ONE_MINUTE = 60000L - - /** - * Enqueues the BackupWorker to run once the constraints are met - */ - fun enqueueBackupWorker( - context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false, - delayMilliseconds: Long = 0L - ) { - val workRequest = buildWorkRequest(requireWifi, requireCharging, delayMilliseconds) - WorkManager.getInstance(context) - .enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.KEEP, workRequest) - Log.d(TAG, "enqueueBackupWorker: BackupWorker enqueued") - } - - /** - * Updates the constraints of an already enqueued BackupWorker - */ - fun updateBackupWorker( - context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false - ) { - try { - val wm = WorkManager.getInstance(context) - val workInfoFuture = wm.getWorkInfosForUniqueWork(TASK_NAME_BACKUP) - val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS) - if (workInfoList != null) { - for (workInfo in workInfoList) { - if (workInfo.state == WorkInfo.State.ENQUEUED) { - val workRequest = buildWorkRequest(requireWifi, requireCharging) - wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest) - Log.d(TAG, "updateBackupWorker updated BackupWorker constraints") - return - } - } - } - Log.d(TAG, "updateBackupWorker: BackupWorker not enqueued") - } catch (e: Exception) { - Log.d(TAG, "updateBackupWorker failed: $e") - } - } - - /** - * Stops the currently running worker (if any) and removes it from the work queue - */ - fun stopWork(context: Context) { - WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_BACKUP) - Log.d(TAG, "stopWork: BackupWorker cancelled") - } - - /** - * Returns `true` if the app is ignoring battery optimizations - */ - fun isIgnoringBatteryOptimizations(ctx: Context): Boolean { - val powerManager = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager - return powerManager.isIgnoringBatteryOptimizations(ctx.packageName) - } - - private fun buildWorkRequest( - requireWifi: Boolean = false, - requireCharging: Boolean = false, - delayMilliseconds: Long = 0L - ): OneTimeWorkRequest { - val constraints = Constraints.Builder() - .setRequiredNetworkType(if (requireWifi) NetworkType.UNMETERED else NetworkType.CONNECTED) - .setRequiresBatteryNotLow(true) - .setRequiresCharging(requireCharging) - .build(); - - val work = OneTimeWorkRequest.Builder(BackupWorker::class.java) - .setConstraints(constraints) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS) - .setInitialDelay(delayMilliseconds, TimeUnit.MILLISECONDS) - .build() - return work - } - - private val flutterLoader = FlutterLoader() - } -} - -private const val TAG = "BackupWorker" diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt deleted file mode 100644 index 9cb2ec7779..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt +++ /dev/null @@ -1,144 +0,0 @@ -package app.alextran.immich - -import android.content.Context -import android.os.SystemClock -import android.provider.MediaStore -import android.util.Log -import androidx.work.Constraints -import androidx.work.Worker -import androidx.work.WorkerParameters -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import androidx.work.Operation -import java.util.concurrent.TimeUnit - -/** - * Worker executed by Android WorkManager observing content changes (new photos/videos) - * - * Immediately enqueues the BackupWorker when running. - * As this work is not triggered periodically, but on content change, the - * worker enqueues itself again after each run. - */ -class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { - - override fun doWork(): Result { - if (!isEnabled(applicationContext)) { - return Result.failure() - } - if (triggeredContentUris.size > 0) { - startBackupWorker(applicationContext, delayMilliseconds = 0) - } - enqueueObserverWorker(applicationContext, ExistingWorkPolicy.REPLACE) - return Result.success() - } - - companion object { - const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled" - private const val SHARED_PREF_REQUIRE_WIFI = "requireWifi" - private const val SHARED_PREF_REQUIRE_CHARGING = "requireCharging" - private const val SHARED_PREF_TRIGGER_UPDATE_DELAY = "triggerUpdateDelay" - private const val SHARED_PREF_TRIGGER_MAX_DELAY = "triggerMaxDelay" - - private const val TASK_NAME_OBSERVER = "immich/ContentObserver" - - /** - * Enqueues the `ContentObserverWorker`. - * - * @param context Android Context - */ - fun enable(context: Context, immediate: Boolean = false) { - enqueueObserverWorker(context, ExistingWorkPolicy.KEEP) - Log.d(TAG, "enabled ContentObserverWorker") - if (immediate) { - startBackupWorker(context, delayMilliseconds = 5000) - } - } - - /** - * Configures the `BackupWorker` to run when all constraints are met. - * - * @param context Android Context - * @param requireWifi if true, task only runs if connected to wifi - * @param requireCharging if true, task only runs if device is charging - */ - fun configureWork(context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false, - triggerUpdateDelay: Long = 5000, - triggerMaxDelay: Long = 50000) { - context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit() - .putBoolean(SHARED_PREF_SERVICE_ENABLED, true) - .putBoolean(SHARED_PREF_REQUIRE_WIFI, requireWifi) - .putBoolean(SHARED_PREF_REQUIRE_CHARGING, requireCharging) - .putLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, triggerUpdateDelay) - .putLong(SHARED_PREF_TRIGGER_MAX_DELAY, triggerMaxDelay) - .apply() - BackupWorker.updateBackupWorker(context, requireWifi, requireCharging) - } - - /** - * Stops the currently running worker (if any) and removes it from the work queue - */ - fun disable(context: Context) { - context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, false).apply() - WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_OBSERVER) - Log.d(TAG, "disabled ContentObserverWorker") - } - - /** - * Return true if the user has enabled the background backup service - */ - fun isEnabled(ctx: Context): Boolean { - return ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getBoolean(SHARED_PREF_SERVICE_ENABLED, false) - } - - /** - * Enqueue and replace the worker without the content trigger but with a short delay - */ - fun workManagerAppClearedWorkaround(context: Context) { - val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java) - .setInitialDelay(500, TimeUnit.MILLISECONDS) - .build() - WorkManager - .getInstance(context) - .enqueueUniqueWork(TASK_NAME_OBSERVER, ExistingWorkPolicy.REPLACE, work) - .result - .get() - Log.d(TAG, "workManagerAppClearedWorkaround") - } - - private fun enqueueObserverWorker(context: Context, policy: ExistingWorkPolicy) { - val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - 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(sp.getLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, 5000), TimeUnit.MILLISECONDS) - .setTriggerContentMaxDelay(sp.getLong(SHARED_PREF_TRIGGER_MAX_DELAY, 50000), TimeUnit.MILLISECONDS) - .build() - - val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java) - .setConstraints(constraints) - .build() - WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work) - } - - fun startBackupWorker(context: Context, delayMilliseconds: Long) { - val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - if (!sp.getBoolean(SHARED_PREF_SERVICE_ENABLED, false)) - return - val requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true) - val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false) - BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds) - sp.edit().putLong(BackupWorker.SHARED_PREF_LAST_CHANGE, SystemClock.uptimeMillis()).apply() - } - - } -} - -private const val TAG = "ContentObserverWorker" 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 4474c63e09..37a325e896 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 @@ -18,8 +18,6 @@ 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) Handler(Looper.getMainLooper()).postDelayed({ // We can only check the engine count and not the status of the lock here, // as the previous start might have been killed without unlocking. 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 06649de8f0..2c80b8d2bd 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 @@ -51,7 +51,6 @@ class MainActivity : FlutterFragmentActivity() { BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx)) ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx)) - flutterEngine.plugins.add(BackgroundServicePlugin()) flutterEngine.plugins.add(backgroundEngineLockImpl) flutterEngine.plugins.add(nativeSyncApiImpl) } 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 index b11b53bcde..bcd7eeee18 100644 --- 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 @@ -43,8 +43,8 @@ class BackgroundEngineLock(context: Context) : BackgroundWorkerLockApi, ImmichPl override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { super.onAttachedToEngine(binding) - checkAndEnforceBackgroundLock(binding.applicationContext) engineCount.incrementAndGet() + checkAndEnforceBackgroundLock(binding.applicationContext) Log.i(TAG, "Flutter engine attached. 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 b6b387db03..0ae49f87f6 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 @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -37,36 +37,150 @@ private object BackgroundWorkerPigeonUtils { ) } } + fun doubleEquals(a: Double, b: Double): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) + } + + fun floatEquals(a: Float, b: Float): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) + } + + fun doubleHash(d: Double): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (d == 0.0) 0.0 else d + val bits = java.lang.Double.doubleToLongBits(normalized) + return (bits xor (bits ushr 32)).toInt() + } + + fun floatHash(f: Float): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (f == 0.0f) 0.0f else f + return java.lang.Float.floatToIntBits(normalized) + } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a === b) { + return true + } + if (a == null || b == null) { + return false + } if (a is ByteArray && b is ByteArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is IntArray && b is IntArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is LongArray && b is LongArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is DoubleArray && b is DoubleArray) { - return a.contentEquals(b) + if (a.size != b.size) return false + for (i in a.indices) { + if (!doubleEquals(a[i], b[i])) return false + } + return true + } + if (a is FloatArray && b is FloatArray) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!floatEquals(a[i], b[i])) return false + } + return true } if (a is Array<*> && b is Array<*>) { - return a.size == b.size && - a.indices.all{ deepEquals(a[it], b[it]) } + if (a.size != b.size) return false + for (i in a.indices) { + if (!deepEquals(a[i], b[i])) return false + } + return true } if (a is List<*> && b is List<*>) { - return a.size == b.size && - a.indices.all{ deepEquals(a[it], b[it]) } + if (a.size != b.size) return false + val iterA = a.iterator() + val iterB = b.iterator() + while (iterA.hasNext() && iterB.hasNext()) { + if (!deepEquals(iterA.next(), iterB.next())) return false + } + return true } 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]) + if (a.size != b.size) return false + for (entry in a) { + val key = entry.key + var found = false + for (bEntry in b) { + if (deepEquals(key, bEntry.key)) { + if (deepEquals(entry.value, bEntry.value)) { + found = true + break + } else { + return false + } + } + } + if (!found) return false } + return true + } + if (a is Double && b is Double) { + return doubleEquals(a, b) + } + if (a is Float && b is Float) { + return floatEquals(a, b) } return a == b } - + + fun deepHash(value: Any?): Int { + return when (value) { + null -> 0 + is ByteArray -> value.contentHashCode() + is IntArray -> value.contentHashCode() + is LongArray -> value.contentHashCode() + is DoubleArray -> { + var result = 1 + for (item in value) { + result = 31 * result + doubleHash(item) + } + result + } + is FloatArray -> { + var result = 1 + for (item in value) { + result = 31 * result + floatHash(item) + } + result + } + is Array<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is List<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is Map<*, *> -> { + var result = 0 + for (entry in value) { + result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) + } + result + } + is Double -> doubleHash(value) + is Float -> floatHash(value) + else -> value.hashCode() + } + } + } /** @@ -79,7 +193,7 @@ class FlutterError ( val code: String, override val message: String? = null, val details: Any? = null -) : Throwable() +) : RuntimeException() /** Generated class from Pigeon that represents data sent in messages. */ data class BackgroundWorkerSettings ( @@ -101,15 +215,22 @@ data class BackgroundWorkerSettings ( ) } override fun equals(other: Any?): Boolean { - if (other !is BackgroundWorkerSettings) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return BackgroundWorkerPigeonUtils.deepEquals(toList(), other.toList()) } + val other = other as BackgroundWorkerSettings + return BackgroundWorkerPigeonUtils.deepEquals(this.requiresCharging, other.requiresCharging) && BackgroundWorkerPigeonUtils.deepEquals(this.minimumDelaySeconds, other.minimumDelaySeconds) + } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + BackgroundWorkerPigeonUtils.deepHash(this.requiresCharging) + result = 31 * result + BackgroundWorkerPigeonUtils.deepHash(this.minimumDelaySeconds) + return result + } } private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { 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 a78db3c5ea..bc0766bee5 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 @@ -5,8 +5,10 @@ import android.provider.MediaStore import android.util.Log import androidx.work.BackoffPolicy import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import io.flutter.embedding.engine.FlutterEngineCache import java.util.concurrent.TimeUnit @@ -18,6 +20,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { override fun enable() { enqueueMediaObserver(ctx) + enqueuePeriodicWorker(ctx) } override fun saveNotificationMessage(title: String, body: String) { @@ -27,12 +30,14 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { override fun configure(settings: BackgroundWorkerSettings) { BackgroundWorkerPreferences(ctx).updateSettings(settings) enqueueMediaObserver(ctx) + enqueuePeriodicWorker(ctx) } override fun disable() { WorkManager.getInstance(ctx).apply { cancelUniqueWork(OBSERVER_WORKER_NAME) cancelUniqueWork(BACKGROUND_WORKER_NAME) + cancelUniqueWork(PERIODIC_WORKER_NAME) } Log.i(TAG, "Cancelled background upload tasks") } @@ -40,6 +45,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { companion object { private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1" private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1" + private const val PERIODIC_WORKER_NAME = "immich/PeriodicBackgroundWorkerV1" const val ENGINE_CACHE_KEY = "immich::background_worker::engine" @@ -55,7 +61,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { setRequiresCharging(settings.requiresCharging) }.build() - val work = OneTimeWorkRequest.Builder(MediaObserver::class.java) + val work = OneTimeWorkRequestBuilder() .setConstraints(constraints) .build() WorkManager.getInstance(ctx) @@ -67,10 +73,30 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { ) } + fun enqueuePeriodicWorker(ctx: Context) { + val settings = BackgroundWorkerPreferences(ctx).getSettings() + val constraints = Constraints.Builder().apply { + setRequiresCharging(settings.requiresCharging) + }.build() + + val work = + PeriodicWorkRequestBuilder( + 1, + TimeUnit.HOURS, + 15, + TimeUnit.MINUTES + ).setConstraints(constraints) + .build() + + WorkManager.getInstance(ctx) + .enqueueUniquePeriodicWork(PERIODIC_WORKER_NAME, ExistingPeriodicWorkPolicy.UPDATE, work) + + Log.i(TAG, "Enqueued periodic background worker with name: $PERIODIC_WORKER_NAME") + } + fun enqueueBackgroundWorker(ctx: Context) { val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build() - - val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java) + val work = OneTimeWorkRequestBuilder() .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) .build() diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt index d7353f0462..4e2e382c2b 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/PeriodicWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/PeriodicWorker.kt new file mode 100644 index 0000000000..d4ecde9bbb --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/PeriodicWorker.kt @@ -0,0 +1,16 @@ +package app.alextran.immich.background + +import android.content.Context +import android.util.Log +import androidx.work.Worker +import androidx.work.WorkerParameters + +class PeriodicWorker(context: Context, params: WorkerParameters) : Worker(context, params) { + private val ctx: Context = context.applicationContext + + override fun doWork(): Result { + Log.i("PeriodicWorker", "Periodic worker triggered, starting background worker") + 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 index 629071382a..aec1f06164 100644 --- 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 @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -46,7 +46,7 @@ class FlutterError ( val code: String, override val message: String? = null, val details: Any? = null -) : Throwable() +) : RuntimeException() enum class NetworkCapability(val raw: Int) { CELLULAR(0), @@ -75,7 +75,7 @@ private open class ConnectivityPigeonCodec : StandardMessageCodec() { when (value) { is NetworkCapability -> { stream.write(129) - writeValue(stream, value.raw) + writeValue(stream, value.raw.toLong()) } else -> super.writeValue(stream, value) } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/HttpClientManager.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/HttpClientManager.kt index e7268396e8..73f7a09183 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/HttpClientManager.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/HttpClientManager.kt @@ -23,10 +23,18 @@ import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.OkHttpClient import org.chromium.net.CronetEngine +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import java.io.ByteArrayInputStream import java.io.File +import java.io.IOException +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes import java.net.Authenticator import java.net.CookieHandler import java.net.PasswordAuthentication @@ -45,7 +53,7 @@ import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509KeyManager import javax.net.ssl.X509TrustManager -const val USER_AGENT = "Immich_Android_${BuildConfig.VERSION_NAME}" +const val USER_AGENT = "immich-android/${BuildConfig.VERSION_NAME}" private const val CERT_ALIAS = "client_cert" private const val PREFS_NAME = "immich.ssl" private const val PREFS_CERT_ALIAS = "immich.client_cert" @@ -277,10 +285,13 @@ object HttpClientManager { return result } - fun rebuildCronetEngine(): CronetEngine { - val old = cronetEngine!! - cronetEngine = buildCronetEngine() - return old + suspend fun rebuildCronetEngine(): Result { + return runCatching { + cronetEngine?.shutdown() + val deletionResult = deleteFolderAndGetSize(cronetStoragePath.toPath()) + cronetEngine = buildCronetEngine() + deletionResult + } } val cronetStoragePath: File get() = cronetStorageDir @@ -301,7 +312,7 @@ object HttpClientManager { } } - private fun buildCronetEngine(): CronetEngine { + fun buildCronetEngine(): CronetEngine { return CronetEngine.Builder(appContext) .enableHttp2(true) .enableQuic(true) @@ -312,6 +323,27 @@ object HttpClientManager { .build() } + private suspend fun deleteFolderAndGetSize(root: Path): Long = withContext(Dispatchers.IO) { + var totalSize = 0L + + Files.walkFileTree(root, object : SimpleFileVisitor() { + override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { + totalSize += attrs.size() + Files.delete(file) + return FileVisitResult.CONTINUE + } + + override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult { + if (dir != root) { + Files.delete(dir) + } + return FileVisitResult.CONTINUE + } + }) + + totalSize + } + private fun build(cacheDir: File): OkHttpClient { val connectionPool = ConnectionPool( maxIdleConnections = KEEP_ALIVE_CONNECTIONS, diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt index 869e312515..1687a7ba95 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -34,36 +34,150 @@ private object NetworkPigeonUtils { ) } } + fun doubleEquals(a: Double, b: Double): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) + } + + fun floatEquals(a: Float, b: Float): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) + } + + fun doubleHash(d: Double): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (d == 0.0) 0.0 else d + val bits = java.lang.Double.doubleToLongBits(normalized) + return (bits xor (bits ushr 32)).toInt() + } + + fun floatHash(f: Float): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (f == 0.0f) 0.0f else f + return java.lang.Float.floatToIntBits(normalized) + } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a === b) { + return true + } + if (a == null || b == null) { + return false + } if (a is ByteArray && b is ByteArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is IntArray && b is IntArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is LongArray && b is LongArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is DoubleArray && b is DoubleArray) { - return a.contentEquals(b) + if (a.size != b.size) return false + for (i in a.indices) { + if (!doubleEquals(a[i], b[i])) return false + } + return true + } + if (a is FloatArray && b is FloatArray) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!floatEquals(a[i], b[i])) return false + } + return true } if (a is Array<*> && b is Array<*>) { - return a.size == b.size && - a.indices.all{ deepEquals(a[it], b[it]) } + if (a.size != b.size) return false + for (i in a.indices) { + if (!deepEquals(a[i], b[i])) return false + } + return true } if (a is List<*> && b is List<*>) { - return a.size == b.size && - a.indices.all{ deepEquals(a[it], b[it]) } + if (a.size != b.size) return false + val iterA = a.iterator() + val iterB = b.iterator() + while (iterA.hasNext() && iterB.hasNext()) { + if (!deepEquals(iterA.next(), iterB.next())) return false + } + return true } 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]) + if (a.size != b.size) return false + for (entry in a) { + val key = entry.key + var found = false + for (bEntry in b) { + if (deepEquals(key, bEntry.key)) { + if (deepEquals(entry.value, bEntry.value)) { + found = true + break + } else { + return false + } + } + } + if (!found) return false } + return true + } + if (a is Double && b is Double) { + return doubleEquals(a, b) + } + if (a is Float && b is Float) { + return floatEquals(a, b) } return a == b } - + + fun deepHash(value: Any?): Int { + return when (value) { + null -> 0 + is ByteArray -> value.contentHashCode() + is IntArray -> value.contentHashCode() + is LongArray -> value.contentHashCode() + is DoubleArray -> { + var result = 1 + for (item in value) { + result = 31 * result + doubleHash(item) + } + result + } + is FloatArray -> { + var result = 1 + for (item in value) { + result = 31 * result + floatHash(item) + } + result + } + is Array<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is List<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is Map<*, *> -> { + var result = 0 + for (entry in value) { + result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) + } + result + } + is Double -> doubleHash(value) + is Float -> floatHash(value) + else -> value.hashCode() + } + } + } /** @@ -76,7 +190,7 @@ class FlutterError ( val code: String, override val message: String? = null, val details: Any? = null -) : Throwable() +) : RuntimeException() /** Generated class from Pigeon that represents data sent in messages. */ data class ClientCertData ( @@ -98,15 +212,22 @@ data class ClientCertData ( ) } override fun equals(other: Any?): Boolean { - if (other !is ClientCertData) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return NetworkPigeonUtils.deepEquals(toList(), other.toList()) } + val other = other as ClientCertData + return NetworkPigeonUtils.deepEquals(this.data, other.data) && NetworkPigeonUtils.deepEquals(this.password, other.password) + } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + NetworkPigeonUtils.deepHash(this.data) + result = 31 * result + NetworkPigeonUtils.deepHash(this.password) + return result + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -135,15 +256,24 @@ data class ClientCertPrompt ( ) } override fun equals(other: Any?): Boolean { - if (other !is ClientCertPrompt) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return NetworkPigeonUtils.deepEquals(toList(), other.toList()) } + val other = other as ClientCertPrompt + return NetworkPigeonUtils.deepEquals(this.title, other.title) && NetworkPigeonUtils.deepEquals(this.message, other.message) && NetworkPigeonUtils.deepEquals(this.cancel, other.cancel) && NetworkPigeonUtils.deepEquals(this.confirm, other.confirm) + } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + NetworkPigeonUtils.deepHash(this.title) + result = 31 * result + NetworkPigeonUtils.deepHash(this.message) + result = 31 * result + NetworkPigeonUtils.deepHash(this.cancel) + result = 31 * result + NetworkPigeonUtils.deepHash(this.confirm) + return result + } } private open class NetworkPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt index 7d998c2f48..e741ce07e9 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -46,7 +46,7 @@ class FlutterError ( val code: String, override val message: String? = null, val details: Any? = null -) : Throwable() +) : RuntimeException() private open class LocalImagesPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return super.readValueOfType(type, buffer) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt index bef6418904..2b5f4d2f57 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt index 8e9fc3f6d5..9255eff44b 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt @@ -21,11 +21,6 @@ import java.io.EOFException import java.io.File import java.io.IOException import java.nio.ByteBuffer -import java.nio.file.FileVisitResult -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.SimpleFileVisitor -import java.nio.file.attribute.BasicFileAttributes import java.util.concurrent.ConcurrentHashMap private class RemoteRequest(val cancellationSignal: CancellationSignal) @@ -205,18 +200,15 @@ private class CronetImageFetcher : ImageFetcher { private fun onDrained() { val onCacheCleared = synchronized(stateLock) { - val onCacheCleared = onCacheCleared + val onCacheCleared = this.onCacheCleared this.onCacheCleared = null onCacheCleared - } - if (onCacheCleared != null) { - val oldEngine = HttpClientManager.rebuildCronetEngine() - oldEngine.shutdown() - CoroutineScope(Dispatchers.IO).launch { - val result = runCatching { deleteFolderAndGetSize(HttpClientManager.cronetStoragePath.toPath()) } - synchronized(stateLock) { draining = false } - onCacheCleared(result) - } + } ?: return + + CoroutineScope(Dispatchers.IO).launch { + val result = HttpClientManager.rebuildCronetEngine() + synchronized(stateLock) { draining = false } + onCacheCleared(result) } } @@ -306,26 +298,6 @@ private class CronetImageFetcher : ImageFetcher { } } - suspend fun deleteFolderAndGetSize(root: Path): Long = withContext(Dispatchers.IO) { - var totalSize = 0L - - Files.walkFileTree(root, object : SimpleFileVisitor() { - override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { - totalSize += attrs.size() - Files.delete(file) - return FileVisitResult.CONTINUE - } - - override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult { - if (dir != root) { - Files.delete(dir) - } - return FileVisitResult.CONTINUE - } - }) - - totalSize - } } private class OkHttpImageFetcher private constructor( diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt index 29c197c2b6..949aa03734 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -34,36 +34,150 @@ private object MessagesPigeonUtils { ) } } + fun doubleEquals(a: Double, b: Double): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) + } + + fun floatEquals(a: Float, b: Float): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) + } + + fun doubleHash(d: Double): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (d == 0.0) 0.0 else d + val bits = java.lang.Double.doubleToLongBits(normalized) + return (bits xor (bits ushr 32)).toInt() + } + + fun floatHash(f: Float): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (f == 0.0f) 0.0f else f + return java.lang.Float.floatToIntBits(normalized) + } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a === b) { + return true + } + if (a == null || b == null) { + return false + } if (a is ByteArray && b is ByteArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is IntArray && b is IntArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is LongArray && b is LongArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is DoubleArray && b is DoubleArray) { - return a.contentEquals(b) + if (a.size != b.size) return false + for (i in a.indices) { + if (!doubleEquals(a[i], b[i])) return false + } + return true + } + if (a is FloatArray && b is FloatArray) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!floatEquals(a[i], b[i])) return false + } + return true } if (a is Array<*> && b is Array<*>) { - return a.size == b.size && - a.indices.all{ deepEquals(a[it], b[it]) } + if (a.size != b.size) return false + for (i in a.indices) { + if (!deepEquals(a[i], b[i])) return false + } + return true } if (a is List<*> && b is List<*>) { - return a.size == b.size && - a.indices.all{ deepEquals(a[it], b[it]) } + if (a.size != b.size) return false + val iterA = a.iterator() + val iterB = b.iterator() + while (iterA.hasNext() && iterB.hasNext()) { + if (!deepEquals(iterA.next(), iterB.next())) return false + } + return true } 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]) + if (a.size != b.size) return false + for (entry in a) { + val key = entry.key + var found = false + for (bEntry in b) { + if (deepEquals(key, bEntry.key)) { + if (deepEquals(entry.value, bEntry.value)) { + found = true + break + } else { + return false + } + } + } + if (!found) return false } + return true + } + if (a is Double && b is Double) { + return doubleEquals(a, b) + } + if (a is Float && b is Float) { + return floatEquals(a, b) } return a == b } - + + fun deepHash(value: Any?): Int { + return when (value) { + null -> 0 + is ByteArray -> value.contentHashCode() + is IntArray -> value.contentHashCode() + is LongArray -> value.contentHashCode() + is DoubleArray -> { + var result = 1 + for (item in value) { + result = 31 * result + doubleHash(item) + } + result + } + is FloatArray -> { + var result = 1 + for (item in value) { + result = 31 * result + floatHash(item) + } + result + } + is Array<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is List<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is Map<*, *> -> { + var result = 0 + for (entry in value) { + result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) + } + result + } + is Double -> doubleHash(value) + is Float -> floatHash(value) + else -> value.hashCode() + } + } + } /** @@ -76,7 +190,7 @@ class FlutterError ( val code: String, override val message: String? = null, val details: Any? = null -) : Throwable() +) : RuntimeException() enum class PlatformAssetPlaybackStyle(val raw: Int) { UNKNOWN(0), @@ -149,15 +263,34 @@ data class PlatformAsset ( ) } override fun equals(other: Any?): Boolean { - if (other !is PlatformAsset) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + val other = other as PlatformAsset + return MessagesPigeonUtils.deepEquals(this.id, other.id) && MessagesPigeonUtils.deepEquals(this.name, other.name) && MessagesPigeonUtils.deepEquals(this.type, other.type) && MessagesPigeonUtils.deepEquals(this.createdAt, other.createdAt) && MessagesPigeonUtils.deepEquals(this.updatedAt, other.updatedAt) && MessagesPigeonUtils.deepEquals(this.width, other.width) && MessagesPigeonUtils.deepEquals(this.height, other.height) && MessagesPigeonUtils.deepEquals(this.durationInSeconds, other.durationInSeconds) && MessagesPigeonUtils.deepEquals(this.orientation, other.orientation) && MessagesPigeonUtils.deepEquals(this.isFavorite, other.isFavorite) && MessagesPigeonUtils.deepEquals(this.adjustmentTime, other.adjustmentTime) && MessagesPigeonUtils.deepEquals(this.latitude, other.latitude) && MessagesPigeonUtils.deepEquals(this.longitude, other.longitude) && MessagesPigeonUtils.deepEquals(this.playbackStyle, other.playbackStyle) + } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.id) + result = 31 * result + MessagesPigeonUtils.deepHash(this.name) + result = 31 * result + MessagesPigeonUtils.deepHash(this.type) + result = 31 * result + MessagesPigeonUtils.deepHash(this.createdAt) + result = 31 * result + MessagesPigeonUtils.deepHash(this.updatedAt) + result = 31 * result + MessagesPigeonUtils.deepHash(this.width) + result = 31 * result + MessagesPigeonUtils.deepHash(this.height) + result = 31 * result + MessagesPigeonUtils.deepHash(this.durationInSeconds) + result = 31 * result + MessagesPigeonUtils.deepHash(this.orientation) + result = 31 * result + MessagesPigeonUtils.deepHash(this.isFavorite) + result = 31 * result + MessagesPigeonUtils.deepHash(this.adjustmentTime) + result = 31 * result + MessagesPigeonUtils.deepHash(this.latitude) + result = 31 * result + MessagesPigeonUtils.deepHash(this.longitude) + result = 31 * result + MessagesPigeonUtils.deepHash(this.playbackStyle) + return result + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -189,15 +322,25 @@ data class PlatformAlbum ( ) } override fun equals(other: Any?): Boolean { - if (other !is PlatformAlbum) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + val other = other as PlatformAlbum + return MessagesPigeonUtils.deepEquals(this.id, other.id) && MessagesPigeonUtils.deepEquals(this.name, other.name) && MessagesPigeonUtils.deepEquals(this.updatedAt, other.updatedAt) && MessagesPigeonUtils.deepEquals(this.isCloud, other.isCloud) && MessagesPigeonUtils.deepEquals(this.assetCount, other.assetCount) + } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.id) + result = 31 * result + MessagesPigeonUtils.deepHash(this.name) + result = 31 * result + MessagesPigeonUtils.deepHash(this.updatedAt) + result = 31 * result + MessagesPigeonUtils.deepHash(this.isCloud) + result = 31 * result + MessagesPigeonUtils.deepHash(this.assetCount) + return result + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -226,15 +369,24 @@ data class SyncDelta ( ) } override fun equals(other: Any?): Boolean { - if (other !is SyncDelta) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + val other = other as SyncDelta + return MessagesPigeonUtils.deepEquals(this.hasChanges, other.hasChanges) && MessagesPigeonUtils.deepEquals(this.updates, other.updates) && MessagesPigeonUtils.deepEquals(this.deletes, other.deletes) && MessagesPigeonUtils.deepEquals(this.assetAlbums, other.assetAlbums) + } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.hasChanges) + result = 31 * result + MessagesPigeonUtils.deepHash(this.updates) + result = 31 * result + MessagesPigeonUtils.deepHash(this.deletes) + result = 31 * result + MessagesPigeonUtils.deepHash(this.assetAlbums) + return result + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -260,15 +412,23 @@ data class HashResult ( ) } override fun equals(other: Any?): Boolean { - if (other !is HashResult) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + val other = other as HashResult + return MessagesPigeonUtils.deepEquals(this.assetId, other.assetId) && MessagesPigeonUtils.deepEquals(this.error, other.error) && MessagesPigeonUtils.deepEquals(this.hash, other.hash) + } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.assetId) + result = 31 * result + MessagesPigeonUtils.deepHash(this.error) + result = 31 * result + MessagesPigeonUtils.deepHash(this.hash) + return result + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -294,15 +454,23 @@ data class CloudIdResult ( ) } override fun equals(other: Any?): Boolean { - if (other !is CloudIdResult) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + val other = other as CloudIdResult + return MessagesPigeonUtils.deepEquals(this.assetId, other.assetId) && MessagesPigeonUtils.deepEquals(this.error, other.error) && MessagesPigeonUtils.deepEquals(this.cloudId, other.cloudId) + } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.assetId) + result = 31 * result + MessagesPigeonUtils.deepHash(this.error) + result = 31 * result + MessagesPigeonUtils.deepHash(this.cloudId) + return result + } } private open class MessagesPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { @@ -344,7 +512,7 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { when (value) { is PlatformAssetPlaybackStyle -> { stream.write(129) - writeValue(stream, value.raw) + writeValue(stream, value.raw.toLong()) } is PlatformAsset -> { stream.write(130) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt index 05671579ae..eea66db2f6 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt @@ -94,11 +94,12 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() { const val HASH_BUFFER_SIZE = 2 * 1024 * 1024 - // _special_format requires S Extensions 21+ + // _special_format: added in API level 37, also in S Extensions 21+ // https://developer.android.com/reference/android/provider/MediaStore.Files.FileColumns#SPECIAL_FORMAT private fun hasSpecialFormatColumn(): Boolean = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && - SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 21 + Build.VERSION.SDK_INT >= 37 || + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 21) } protected fun getCursor( diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle index 719c946bd6..2663154d9d 100644 --- a/mobile/android/build.gradle +++ b/mobile/android/build.gradle @@ -1,6 +1,4 @@ allprojects { - ext.kotlin_version = '2.2.20' - repositories { google() mavenCentral() @@ -10,22 +8,7 @@ allprojects { rootProject.buildDir = '../build' subprojects { - // fix for verifyReleaseResources - // ============ - afterEvaluate { project -> - if (project.plugins.hasPlugin("com.android.application") || - project.plugins.hasPlugin("com.android.library")) { - project.android { - compileSdkVersion 36 - buildToolsVersion "36.0.0" - } - } - } - // ============ project.buildDir = "${rootProject.buildDir}/${project.name}" -} - -subprojects { project.evaluationDependsOn(':app') } @@ -36,4 +19,3 @@ tasks.register("clean", Delete) { tasks.named('wrapper') { distributionType = Wrapper.DistributionType.ALL } - diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 4e56a3fc55..7312a8ca68 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" => 3038, - "android.injected.version.name" => "2.6.0", + "android.injected.version.code" => 3046, + "android.injected.version.name" => "2.7.5", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/android/gradle/libs.versions.toml b/mobile/android/gradle/libs.versions.toml new file mode 100644 index 0000000000..fa4ba34a2d --- /dev/null +++ b/mobile/android/gradle/libs.versions.toml @@ -0,0 +1,51 @@ +[versions] +agp = "8.11.2" +kotlin = "2.2.20" +ksp = "2.2.20-2.0.3" +coroutines = "1.9.0" +work = "2.9.1" +concurrent = "1.2.0" +guava = "33.3.1-android" +glide = "4.16.0" +serialization-json = "1.8.1" +glance = "1.1.1" +gson = "2.10.1" +okhttp = "4.12.0" +cronet = "143.7445.0" +media3 = "1.10.0" +desugar = "2.1.2" +activity-compose = "1.8.2" +compose-ui = "1.1.1" +material3 = "1.2.1" +lifecycle = "2.6.2" +material = "1.12.0" + +[libraries] +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +cronet-embedded = { module = "org.chromium.net:cronet-embedded", version.ref = "cronet" } +media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" } +media3-datasource-cronet = { module = "androidx.media3:media3-datasource-cronet", version.ref = "media3" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "work" } +concurrent-futures = { module = "androidx.concurrent:concurrent-futures", version.ref = "concurrent" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } +glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } +glide-ksp = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization-json" } +desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar" } +glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } +compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-ui" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-ui" } +compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } +lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } +material = { module = "com.google.android.material:material", version.ref = "material" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +# TODO: update to version.ref = "kotlin" when background_downloader is removed +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.1.0" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/mobile/android/gradle/wrapper/gradle-wrapper.properties b/mobile/android/gradle/wrapper/gradle-wrapper.properties index ed4c299adb..6514f919fd 100644 --- a/mobile/android/gradle/wrapper/gradle-wrapper.properties +++ b/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle index fbed55a3e3..d6555f43b1 100644 --- a/mobile/android/settings.gradle +++ b/mobile/android/settings.gradle @@ -18,10 +18,11 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.11.2' apply false + id "com.android.application" version "8.11.2" apply false id "org.jetbrains.kotlin.android" version "2.2.20" apply false - id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22' apply false - id 'com.google.devtools.ksp' version '2.2.20-2.0.3' apply false + // TODO: update to match kotlin version when background_downloader is removed + id "org.jetbrains.kotlin.plugin.serialization" version "2.1.0" apply false + id "com.google.devtools.ksp" version "2.2.20-2.0.3" apply false } include ":app" diff --git a/mobile/dart_test.yaml b/mobile/dart_test.yaml deleted file mode 100644 index fa54954090..0000000000 --- a/mobile/dart_test.yaml +++ /dev/null @@ -1,3 +0,0 @@ -# Used to filter out tags from test runs -tags: - widget: diff --git a/mobile/immich_lint/analysis_options.yaml b/mobile/immich_lint/analysis_options.yaml deleted file mode 100644 index 572dd239d0..0000000000 --- a/mobile/immich_lint/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: package:lints/recommended.yaml diff --git a/mobile/immich_lint/lib/immich_mobile_immich_lint.dart b/mobile/immich_lint/lib/immich_mobile_immich_lint.dart deleted file mode 100644 index 7d3ed4757e..0000000000 --- a/mobile/immich_lint/lib/immich_mobile_immich_lint.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:analyzer/error/error.dart' show ErrorSeverity; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -// ignore: depend_on_referenced_packages -import 'package:glob/glob.dart'; - -PluginBase createPlugin() => ImmichLinter(); - -class ImmichLinter extends PluginBase { - @override - List getLintRules(CustomLintConfigs configs) { - final List rules = []; - for (final entry in configs.rules.entries) { - if (entry.value.enabled && entry.key.startsWith("import_rule_")) { - final code = makeCode(entry.key, entry.value); - final allowedPaths = getStrings(entry.value, "allowed"); - final forbiddenPaths = getStrings(entry.value, "forbidden"); - final restrict = getStrings(entry.value, "restrict"); - rules.add(ImportRule(code, buildGlob(allowedPaths), - buildGlob(forbiddenPaths), restrict)); - } - } - return rules; - } - - static LintCode makeCode(String name, LintOptions options) => LintCode( - name: name, - problemMessage: options.json["message"] as String, - errorSeverity: ErrorSeverity.WARNING, - ); - - static List getStrings(LintOptions options, String field) { - final List result = []; - final excludeOption = options.json[field]; - if (excludeOption is String) { - result.add(excludeOption); - } else if (excludeOption is List) { - result.addAll(excludeOption.map((option) => option)); - } - return result; - } - - Glob? buildGlob(List globs) { - if (globs.isEmpty) return null; - if (globs.length == 1) return Glob(globs[0], caseSensitive: true); - return Glob("{${globs.join(",")}}", caseSensitive: true); - } -} - -// ignore: must_be_immutable -class ImportRule extends DartLintRule { - ImportRule(LintCode code, this._allowed, this._forbidden, this._restrict) - : super(code: code); - - final Glob? _allowed; - final Glob? _forbidden; - final List _restrict; - int _rootOffset = -1; - - @override - void run( - CustomLintResolver resolver, - ErrorReporter reporter, - CustomLintContext context, - ) { - if (_rootOffset == -1) { - const project = "/immich/mobile/"; - _rootOffset = - resolver.path.toLowerCase().indexOf(project) + project.length; - } - final path = resolver.path.substring(_rootOffset); - - if ((_allowed != null && _allowed!.matches(path)) && - (_forbidden == null || !_forbidden!.matches(path))) { - return; - } - - context.registry.addImportDirective((node) { - final uri = node.uri.stringValue; - if (uri == null) return; - for (final restricted in _restrict) { - if (uri.startsWith(restricted) == true) { - reporter.atNode(node, code); - return; - } - } - }); - } -} diff --git a/mobile/immich_lint/pubspec.lock b/mobile/immich_lint/pubspec.lock deleted file mode 100644 index 0e4b08be87..0000000000 --- a/mobile/immich_lint/pubspec.lock +++ /dev/null @@ -1,365 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f - url: "https://pub.dev" - source: hosted - version: "82.0.0" - analyzer: - dependency: "direct main" - description: - name: analyzer - sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" - url: "https://pub.dev" - source: hosted - version: "7.4.5" - analyzer_plugin: - dependency: "direct main" - description: - name: analyzer_plugin - sha256: ee188b6df6c85f1441497c7171c84f1392affadc0384f71089cb10a3bc508cef - url: "https://pub.dev" - source: hosted - version: "0.13.1" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" - source: hosted - version: "2.0.4" - ci: - dependency: transitive - description: - name: ci - sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" - url: "https://pub.dev" - source: hosted - version: "0.1.0" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.dev" - source: hosted - version: "0.4.2" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - crypto: - dependency: transitive - description: - name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" - url: "https://pub.dev" - source: hosted - version: "3.0.6" - custom_lint: - dependency: transitive - description: - name: custom_lint - sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1" - url: "https://pub.dev" - source: hosted - version: "0.7.5" - custom_lint_builder: - dependency: "direct main" - description: - name: custom_lint_builder - sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228" - url: "https://pub.dev" - source: hosted - version: "0.7.5" - custom_lint_core: - dependency: transitive - description: - name: custom_lint_core - sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" - url: "https://pub.dev" - source: hosted - version: "0.7.5" - custom_lint_visitor: - dependency: transitive - description: - name: custom_lint_visitor - sha256: cba5b6d7a6217312472bf4468cdf68c949488aed7ffb0eab792cd0b6c435054d - url: "https://pub.dev" - source: hosted - version: "1.0.0+7.4.5" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - freezed_annotation: - dependency: transitive - description: - name: freezed_annotation - sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - glob: - dependency: "direct main" - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - hotreloader: - dependency: transitive - description: - name: hotreloader - sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b - url: "https://pub.dev" - source: hosted - version: "4.3.0" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.dev" - source: hosted - version: "4.9.0" - lints: - dependency: "direct dev" - description: - name: lints - sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 - url: "https://pub.dev" - source: hosted - version: "6.0.0" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" - url: "https://pub.dev" - source: hosted - version: "1.5.0" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.dev" - source: hosted - version: "2.1.1" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" - url: "https://pub.dev" - source: hosted - version: "0.7.6" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff - url: "https://pub.dev" - source: hosted - version: "4.5.1" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - watcher: - dependency: transitive - description: - name: watcher - sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.8.0 <4.0.0" diff --git a/mobile/immich_lint/pubspec.yaml b/mobile/immich_lint/pubspec.yaml deleted file mode 100644 index e49e9c5010..0000000000 --- a/mobile/immich_lint/pubspec.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: immich_mobile_immich_lint -publish_to: none - -environment: - sdk: '>=3.0.0 <4.0.0' - -dependencies: - analyzer: ^7.0.0 - analyzer_plugin: ^0.13.0 - custom_lint_builder: ^0.7.5 - glob: ^2.1.2 - -dev_dependencies: - lints: ^6.0.0 diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart index d6065170ef..66955364f3 100644 --- a/mobile/integration_test/test_utils/general_helper.dart +++ b/mobile/integration_test/test_utils/general_helper.dart @@ -5,7 +5,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/main.dart' as app; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:integration_test/integration_test.dart'; @@ -39,20 +38,11 @@ class ImmichTestHelper { static Future loadApp(WidgetTester tester) async { await EasyLocalization.ensureInitialized(); // Clear all data from Isar (reuse existing instance if available) - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); + final (drift, _) = await Bootstrap.initDomain(); await Store.clear(); - await isar.writeTxn(() => isar.clear()); // Load main Widget await tester.pumpWidget( - ProviderScope( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - child: const app.MainWidget(), - ), + ProviderScope(overrides: [driftProvider.overrideWith(driftOverride(drift))], child: const app.MainWidget()), ); // Post run tasks await EasyLocalization.ensureInitialized(); diff --git a/mobile/ios/Flutter/AppFrameworkInfo.plist b/mobile/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf7652..391a902b2b 100644 --- a/mobile/ios/Flutter/AppFrameworkInfo.plist +++ b/mobile/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index e1ec4aff07..c566d37182 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -20,21 +20,21 @@ PODS: - Flutter - flutter_udid (0.0.1): - Flutter - - SAMKeychain - - flutter_web_auth_2 (3.0.0): + - KeychainAccess + - flutter_web_auth_2 (5.0.0): - Flutter - fluttertoast (0.0.2): - Flutter - geolocator_apple (1.2.0): - Flutter + - FlutterMacOS - home_widget (0.0.1): - Flutter - image_picker_ios (0.0.1): - Flutter - integration_test (0.0.1): - Flutter - - isar_community_flutter_libs (1.0.0): - - Flutter + - KeychainAccess (4.2.2) - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS @@ -46,19 +46,13 @@ PODS: - Flutter - network_info_plus (0.0.1): - Flutter - - objective_c (0.0.1): - - Flutter - package_info_plus (0.4.5): - Flutter - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter - - photo_manager (3.7.1): + - photo_manager (3.9.0): - Flutter - FlutterMacOS - - SAMKeychain (1.5.3) - share_handler_ios (0.0.14): - Flutter - share_handler_ios/share_handler_ios_models (= 0.0.14) @@ -72,28 +66,6 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqflite_darwin (0.0.4): - - Flutter - - FlutterMacOS - - sqlite3 (3.49.1): - - sqlite3/common (= 3.49.1) - - sqlite3/common (3.49.1) - - sqlite3/dbstatvtab (3.49.1): - - sqlite3/common - - sqlite3/fts5 (3.49.1): - - sqlite3/common - - sqlite3/perf-threadsafe (3.49.1): - - sqlite3/common - - sqlite3/rtree (3.49.1): - - sqlite3/common - - sqlite3_flutter_libs (0.0.1): - - Flutter - - FlutterMacOS - - sqlite3 (~> 3.49.1) - - sqlite3/dbstatvtab - - sqlite3/fts5 - - sqlite3/perf-threadsafe - - sqlite3/rtree - url_launcher_ios (0.0.1): - Flutter - wakelock_plus (0.0.1): @@ -112,34 +84,28 @@ DEPENDENCIES: - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) - flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - - geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`) + - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) - home_widget (from `.symlinks/plugins/home_widget/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - - isar_community_flutter_libs (from `.symlinks/plugins/isar_community_flutter_libs/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`) - native_video_player (from `.symlinks/plugins/native_video_player/ios`) - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`) - - objective_c (from `.symlinks/plugins/objective_c/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - photo_manager (from `.symlinks/plugins/photo_manager/ios`) + - photo_manager (from `.symlinks/plugins/photo_manager/darwin`) - share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`) - share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) SPEC REPOS: trunk: + - KeychainAccess - MapLibre - - SAMKeychain - - sqlite3 EXTERNAL SOURCES: background_downloader: @@ -167,15 +133,13 @@ EXTERNAL SOURCES: fluttertoast: :path: ".symlinks/plugins/fluttertoast/ios" geolocator_apple: - :path: ".symlinks/plugins/geolocator_apple/ios" + :path: ".symlinks/plugins/geolocator_apple/darwin" home_widget: :path: ".symlinks/plugins/home_widget/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" - isar_community_flutter_libs: - :path: ".symlinks/plugins/isar_community_flutter_libs/ios" local_auth_darwin: :path: ".symlinks/plugins/local_auth_darwin/darwin" maplibre_gl: @@ -184,16 +148,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/native_video_player/ios" network_info_plus: :path: ".symlinks/plugins/network_info_plus/ios" - objective_c: - :path: ".symlinks/plugins/objective_c/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" - path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" photo_manager: - :path: ".symlinks/plugins/photo_manager/ios" + :path: ".symlinks/plugins/photo_manager/darwin" share_handler_ios: :path: ".symlinks/plugins/share_handler_ios/ios" share_handler_ios_models: @@ -202,10 +162,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sqflite_darwin: - :path: ".symlinks/plugins/sqflite_darwin/darwin" - sqlite3_flutter_libs: - :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" wakelock_plus: @@ -221,33 +177,27 @@ SPEC CHECKSUMS: flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 - flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 - flutter_web_auth_2: 5c8d9dcd7848b5a9efb086d24e7a9adcae979c80 + flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300 + flutter_web_auth_2: 646fc9df97a01c59e5eea99b237da2c6360f8439 fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 - geolocator_apple: 1560c3c875af2a412242c7a923e15d0d401966ff + geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - isar_community_flutter_libs: bede843185a61a05ff364a05c9b23209523f7e0d - local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 + KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51 + local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f native_video_player: b65c58951ede2f93d103a25366bdebca95081265 network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc - objective_c: 89e720c30d716b036faf9c9684022048eee1eee2 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62 - SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c + photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 - sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 PODFILE CHECKSUM: 938abbae4114b9c2140c550a2a0d8f7c674f5dfe diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 22a7abcbac..f88d624b89 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -3,19 +3,18 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */; }; - 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */; }; - 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A01DD6982F7F43B40049AB63 /* ImageRequest.swift */; }; 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 */; }; @@ -89,8 +88,6 @@ 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; - 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundServicePlugin.swift; sourceTree = ""; }; - 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundSyncWorker.swift; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -102,6 +99,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A01DD6982F7F43B40049AB63 /* ImageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRequest.swift; sourceTree = ""; }; B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; }; B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = ""; }; B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = ""; }; @@ -137,6 +135,13 @@ ); target = F0B57D372DF764BD00DC5BCC /* WidgetExtension */; }; + FE1BB4572F83196E0087DBF9 /* Exceptions for "Utility" folder in "Runner" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Mutex.swift, + ); + target = 97C146ED1CF9000F007C117D /* Runner */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -162,6 +167,14 @@ path = WidgetExtension; sourceTree = ""; }; + FE1BB4562F8319560087DBF9 /* Utility */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + FE1BB4572F83196E0087DBF9 /* Exceptions for "Utility" folder in "Runner" target */, + ); + path = Utility; + sourceTree = ""; + }; FEE084F22EC172080045228E /* Schemas */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -227,15 +240,6 @@ name = Frameworks; sourceTree = ""; }; - 65DD438629917FAD0047FFA8 /* BackgroundSync */ = { - isa = PBXGroup; - children = ( - 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */, - 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */, - ); - path = BackgroundSync; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -273,13 +277,13 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + FE1BB4562F8319560087DBF9 /* Utility */, FEE084F22EC172080045228E /* Schemas */, B231F52D2E93A44A00BC45D1 /* Core */, B25D37792E72CA15008B6CA7 /* Connectivity */, B21E34A62E5AF9760031FDB9 /* Background */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */, FA9973382CF6DF4B000EF859 /* Runner.entitlements */, - 65DD438629917FAD0047FFA8 /* BackgroundSync */, FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -327,6 +331,7 @@ FED3B1952E253E9B0030FD97 /* Images */ = { isa = PBXGroup; children = ( + A01DD6982F7F43B40049AB63 /* ImageRequest.swift */, FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */, FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */, FE5499F52F11980E006016CB /* LocalImagesImpl.swift */, @@ -606,8 +611,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */, B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */, FE5499F32F1197D8006016CB /* LocalImages.g.swift in Sources */, FE5499F62F11980E006016CB /* LocalImagesImpl.swift in Sources */, @@ -620,7 +625,6 @@ B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */, - 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1239,7 +1243,7 @@ repositoryURL = "https://github.com/pointfreeco/sqlite-data"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.3.0; + minimumVersion = 1.6.1; }; }; FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */ = { @@ -1247,7 +1251,7 @@ repositoryURL = "https://github.com/apple/swift-http-structured-headers.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.5.0; + minimumVersion = 1.6.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 432e81234d..187a67cb27 100644 --- a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -19,31 +19,13 @@ "version" : "7.8.0" } }, - { - "identity" : "opencombine", - "kind" : "remoteSourceControl", - "location" : "https://github.com/OpenCombine/OpenCombine.git", - "state" : { - "revision" : "8576f0d579b27020beccbccc3ea6844f3ddfc2c2", - "version" : "0.14.0" - } - }, { "identity" : "sqlite-data", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/sqlite-data", "state" : { - "revision" : "b66b894b9a5710f1072c8eb6448a7edfc2d743d9", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-case-paths", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-case-paths", - "state" : { - "revision" : "6989976265be3f8d2b5802c722f9ba168e227c71", - "version" : "1.7.2" + "revision" : "da3a94ed49c7a30d82853de551c07a93196e8cab", + "version" : "1.6.1" } }, { @@ -96,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-structured-headers.git", "state" : { - "revision" : "a9f3c352f4d46afd155e00b3c6e85decae6bcbeb", - "version" : "1.5.0" + "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", + "version" : "1.6.0" } }, { @@ -141,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "revision" : "9c84335373bae5f5c9f7b5f0adf3ae10f2cab5b9", - "version" : "0.25.2" + "revision" : "8da8818fccd9959bd683934ddc62cf45bb65b3c8", + "version" : "0.31.1" } }, { @@ -154,15 +136,6 @@ "version" : "602.0.0" } }, - { - "identity" : "swift-tagged", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-tagged", - "state" : { - "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", - "version" : "0.10.0" - } - }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4962230c22..800ff8ac52 100644 --- a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/sqlite-data", "state" : { - "revision" : "05704b563ecb7f0bd7e49b6f360a6383a3e53e7d", - "version" : "1.5.1" + "revision" : "da3a94ed49c7a30d82853de551c07a93196e8cab", + "version" : "1.6.1" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-structured-headers.git", "state" : { - "revision" : "a9f3c352f4d46afd155e00b3c6e85decae6bcbeb", - "version" : "1.5.0" + "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", + "version" : "1.6.0" } }, { @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "revision" : "d8163b3a98f3c8434c4361e85126db449d84bc66", - "version" : "0.30.0" + "revision" : "8da8818fccd9959bd683934ddc62cf45bb65b3c8", + "version" : "0.31.1" } }, { diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 81af41ab08..216146a6f3 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -1,15 +1,7 @@ -import BackgroundTasks -import Flutter import native_video_player -import network_info_plus -import path_provider_foundation -import permission_handler_apple -import photo_manager -import shared_preferences_foundation -import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? @@ -21,48 +13,26 @@ import UIKit SwiftNativeVideoPlayerPlugin.cookieStorage = URLSessionManager.cookieStorage URLSessionManager.patchBackgroundDownloader() - GeneratedPluginRegistrant.register(with: self) - let controller: FlutterViewController = window?.rootViewController as! FlutterViewController - AppDelegate.registerPlugins(with: controller.engine, controller: controller) - BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) - - BackgroundServicePlugin.registerBackgroundProcessing() BackgroundWorkerApiImpl.registerBackgroundWorkers() - BackgroundServicePlugin.setPluginRegistrantCallback { registry in - if !registry.hasPlugin("org.cocoapods.path-provider-foundation") { - PathProviderPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.path-provider-foundation")!) - } - - if !registry.hasPlugin("org.cocoapods.photo-manager") { - PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!) - } - - if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") { - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!) - } - - if !registry.hasPlugin("org.cocoapods.permission-handler-apple") { - PermissionHandlerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!) - } - - if !registry.hasPlugin("org.cocoapods.network-info-plus") { - FPPNetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.network-info-plus")!) - } - } - return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - - public static func registerPlugins(with engine: FlutterEngine, controller: FlutterViewController?) { - NativeSyncApiImpl.register(with: engine.registrar(forPlugin: NativeSyncApiImpl.name)!) - LocalImageApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: LocalImageApiImpl()) - RemoteImageApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: RemoteImageApiImpl()) - BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: BackgroundWorkerApiImpl()) - ConnectivityApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: ConnectivityApiImpl()) - NetworkApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: NetworkApiImpl(viewController: controller)) + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + let messenger = engineBridge.applicationRegistrar.messenger() + AppDelegate.registerPlugins(with: engineBridge.pluginRegistry, messenger: messenger) } - + + public static func registerPlugins(with registry: FlutterPluginRegistry, messenger: FlutterBinaryMessenger) { + NativeSyncApiImpl.register(with: registry.registrar(forPlugin: NativeSyncApiImpl.name)!) + LocalImageApiSetup.setUp(binaryMessenger: messenger, api: LocalImageApiImpl()) + RemoteImageApiSetup.setUp(binaryMessenger: messenger, api: RemoteImageApiImpl()) + BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: messenger, api: BackgroundWorkerApiImpl()) + ConnectivityApiSetup.setUp(binaryMessenger: messenger, api: ConnectivityApiImpl()) + NetworkApiSetup.setUp(binaryMessenger: messenger, api: NetworkApiImpl()) + } + public static func cancelPlugins(with engine: FlutterEngine) { (engine.valuePublished(byPlugin: NativeSyncApiImpl.name) as? NativeSyncApiImpl)?.detachFromEngine() } diff --git a/mobile/ios/Runner/Background/BackgroundWorker.g.swift b/mobile/ios/Runner/Background/BackgroundWorker.g.swift index 8c9391e8d2..40553441a6 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.g.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.g.swift @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] { } return [ "\(error)", - "\(type(of: error))", + "\(Swift.type(of: error))", "Stacktrace: \(Thread.callStackSymbols)", ] } @@ -50,6 +50,19 @@ private func nilOrValue(_ value: Any?) -> T? { return value as! T? } +private func doubleEqualsBackgroundWorker(_ lhs: Double, _ rhs: Double) -> Bool { + return (lhs.isNaN && rhs.isNaN) || lhs == rhs +} + +private func doubleHashBackgroundWorker(_ value: Double, _ hasher: inout Hasher) { + if value.isNaN { + hasher.combine(0x7FF8000000000000) + } else { + // Normalize -0.0 to 0.0 + hasher.combine(value == 0 ? 0 : value) + } +} + func deepEqualsBackgroundWorker(_ lhs: Any?, _ rhs: Any?) -> Bool { let cleanLhs = nilOrValue(lhs) as Any? let cleanRhs = nilOrValue(rhs) as Any? @@ -60,59 +73,92 @@ func deepEqualsBackgroundWorker(_ lhs: Any?, _ rhs: Any?) -> Bool { case (nil, _), (_, nil): return false + case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: + return true + 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]) { + case (let lhsArray, let rhsArray) as ([Any?], [Any?]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !deepEqualsBackgroundWorker(element, rhsArray[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]!) { + case (let lhsArray, let rhsArray) as ([Double], [Double]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !doubleEqualsBackgroundWorker(element, rhsArray[index]) { return false } } return true + case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard lhsDictionary.count == rhsDictionary.count else { return false } + for (lhsKey, lhsValue) in lhsDictionary { + var found = false + for (rhsKey, rhsValue) in rhsDictionary { + if deepEqualsBackgroundWorker(lhsKey, rhsKey) { + if deepEqualsBackgroundWorker(lhsValue, rhsValue) { + found = true + break + } else { + return false + } + } + } + if !found { return false } + } + return true + + case (let lhs as Double, let rhs as Double): + return doubleEqualsBackgroundWorker(lhs, rhs) + + case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): + return lhsHashable == rhsHashable + 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) + let cleanValue = nilOrValue(value) as Any? + if let cleanValue = cleanValue { + if let doubleValue = cleanValue as? Double { + doubleHashBackgroundWorker(doubleValue, &hasher) + } else if let valueList = cleanValue as? [Any?] { + for item in valueList { + deepHashBackgroundWorker(value: item, hasher: &hasher) + } + } else if let valueList = cleanValue as? [Double] { + for item in valueList { + doubleHashBackgroundWorker(item, &hasher) + } + } else if let valueDict = cleanValue as? [AnyHashable: Any?] { + var result = 0 + for (key, value) in valueDict { + var entryKeyHasher = Hasher() + deepHashBackgroundWorker(value: key, hasher: &entryKeyHasher) + var entryValueHasher = Hasher() + deepHashBackgroundWorker(value: value, hasher: &entryValueHasher) + result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) + } + hasher.combine(result) + } else if let hashableValue = cleanValue as? AnyHashable { + hasher.combine(hashableValue) + } else { + hasher.combine(String(describing: cleanValue)) } - return + } else { + hasher.combine(0) } - - 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 { @@ -137,9 +183,16 @@ struct BackgroundWorkerSettings: Hashable { ] } static func == (lhs: BackgroundWorkerSettings, rhs: BackgroundWorkerSettings) -> Bool { - return deepEqualsBackgroundWorker(lhs.toList(), rhs.toList()) } + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsBackgroundWorker(lhs.requiresCharging, rhs.requiresCharging) && deepEqualsBackgroundWorker(lhs.minimumDelaySeconds, rhs.minimumDelaySeconds) + } + func hash(into hasher: inout Hasher) { - deepHashBackgroundWorker(value: toList(), hasher: &hasher) + hasher.combine("BackgroundWorkerSettings") + deepHashBackgroundWorker(value: requiresCharging, hasher: &hasher) + deepHashBackgroundWorker(value: minimumDelaySeconds, hasher: &hasher) } } diff --git a/mobile/ios/Runner/Background/BackgroundWorker.swift b/mobile/ios/Runner/Background/BackgroundWorker.swift index 85e1a55d3d..c5b5e1778a 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.swift @@ -95,7 +95,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { // Register plugins in the new engine GeneratedPluginRegistrant.register(with: engine) // Register custom plugins - AppDelegate.registerPlugins(with: engine, controller: nil) + AppDelegate.registerPlugins(with: engine, messenger: engine.binaryMessenger) flutterApi = BackgroundWorkerFlutterApi(binaryMessenger: engine.binaryMessenger) BackgroundWorkerBgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: self) diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift b/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift deleted file mode 100644 index cac9faab01..0000000000 --- a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift +++ /dev/null @@ -1,408 +0,0 @@ -// -// BackgroundServicePlugin.swift -// Runner -// -// Created by Marty Fuhry on 2/14/23. -// - -import Flutter -import BackgroundTasks -import path_provider_foundation -import CryptoKit -import Network - -class BackgroundServicePlugin: NSObject, FlutterPlugin { - - public static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback? - - public static func setPluginRegistrantCallback(_ callback: FlutterPluginRegistrantCallback) { - flutterPluginRegistrantCallback = callback - } - - // Pause the application in XCode, then enter - // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundFetch"] - // or - // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundProcessing"] - // Then resume the application see the background code run - // Tested on a physical device, not a simulator - // This will submit either the Fetch or Processing command to the BGTaskScheduler for immediate processing. - // In my tests, I can only get app.alextran.immich.backgroundProcessing simulated by running the above command - - // This is the task ID in Info.plist to register as our background task ID - public static let backgroundFetchTaskID = "app.alextran.immich.backgroundFetch" - public static let backgroundProcessingTaskID = "app.alextran.immich.backgroundProcessing" - - // Establish communication with the main isolate and set up the channel call - // to this BackgroundServicePlugion() - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: "immich/foregroundChannel", - binaryMessenger: registrar.messenger() - ) - - let instance = BackgroundServicePlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - registrar.addApplicationDelegate(instance) - } - - // Registers the Flutter engine with the plugins, used by the other Background Flutter engine - public static func register(engine: FlutterEngine) { - GeneratedPluginRegistrant.register(with: engine) - } - - // Registers the task IDs from the system so that we can process them here in this class - public static func registerBackgroundProcessing() { - - let processingRegisterd = BGTaskScheduler.shared.register( - forTaskWithIdentifier: backgroundProcessingTaskID, - using: nil) { task in - if task is BGProcessingTask { - handleBackgroundProcessing(task: task as! BGProcessingTask) - } - } - - let fetchRegisterd = BGTaskScheduler.shared.register( - forTaskWithIdentifier: backgroundFetchTaskID, - using: nil) { task in - if task is BGAppRefreshTask { - handleBackgroundFetch(task: task as! BGAppRefreshTask) - } - } - } - - // Handles the channel methods from Flutter - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "enable": - handleBackgroundEnable(call: call, result: result) - break - case "configure": - handleConfigure(call: call, result: result) - break - case "disable": - handleDisable(call: call, result: result) - break - case "isEnabled": - handleIsEnabled(call: call, result: result) - break - case "isIgnoringBatteryOptimizations": - result(FlutterMethodNotImplemented) - break - case "lastBackgroundFetchTime": - let defaults = UserDefaults.standard - let lastRunTime = defaults.value(forKey: "last_background_fetch_run_time") - result(lastRunTime) - break - case "lastBackgroundProcessingTime": - let defaults = UserDefaults.standard - let lastRunTime = defaults.value(forKey: "last_background_processing_run_time") - result(lastRunTime) - break - case "numberOfBackgroundProcesses": - handleNumberOfProcesses(call: call, result: result) - break - case "backgroundAppRefreshEnabled": - handleBackgroundRefreshStatus(call: call, result: result) - break - case "digestFiles": - handleDigestFiles(call: call, result: result) - break - default: - result(FlutterMethodNotImplemented) - break - } - } - - // Calculates the SHA-1 hash of each file from the list of paths provided - func handleDigestFiles(call: FlutterMethodCall, result: @escaping FlutterResult) { - - let bufsize = 2 * 1024 * 1024 - // Private error to throw if file cannot be read - enum DigestError: String, LocalizedError { - case NoFileHandle = "Cannot Open File Handle" - - public var errorDescription: String? { self.rawValue } - } - - // Parse the arguments or else fail - guard let args = call.arguments as? Array else { - print("Cannot parse args as array: \(String(describing: call.arguments))") - result(FlutterError(code: "Malformed", - message: "Received args is not an Array", - details: nil)) - return - } - - // Compute hash in background thread - DispatchQueue.global(qos: .background).async { - var hashes: [FlutterStandardTypedData?] = Array(repeating: nil, count: args.count) - for i in (0 ..< args.count) { - do { - guard let file = FileHandle(forReadingAtPath: args[i]) else { throw DigestError.NoFileHandle } - var hasher = Insecure.SHA1.init(); - while autoreleasepool(invoking: { - let chunk = file.readData(ofLength: bufsize) - guard !chunk.isEmpty else { return false } // EOF - hasher.update(data: chunk) - return true // continue - }) { } - let digest = hasher.finalize() - hashes[i] = FlutterStandardTypedData(bytes: Data(Array(digest.makeIterator()))) - } catch { - print("Cannot calculate the digest of the file \(args[i]) due to \(error.localizedDescription)") - } - } - - // Return result in main thread - DispatchQueue.main.async { - result(Array(hashes)) - } - } - } - - // Called by the flutter code when enabled so that we can turn on the background services - // and save the callback information to communicate on this method channel - public func handleBackgroundEnable(call: FlutterMethodCall, result: FlutterResult) { - - // Needs to parse the arguments from the method call - guard let args = call.arguments as? Array else { - print("Cannot parse args as array: \(call.arguments)") - result(FlutterMethodNotImplemented) - return - } - - // Requires 3 arguments in the array - guard args.count == 3 else { - print("Requires 3 arguments and received \(args.count)") - result(FlutterMethodNotImplemented) - return - } - - // Parses the arguments - let callbackHandle = args[0] as? Int64 - let notificationTitle = args[1] as? String - let instant = args[2] as? Bool - - // Write enabled to settings - let defaults = UserDefaults.standard - - // We are now enabled, so store this - defaults.set(true, forKey: "background_service_enabled") - - // The callback handle is an int64 address to communicate with the main isolate's - // entry function - defaults.set(callbackHandle, forKey: "callback_handle") - - // This is not used yet and will need to be implemented - defaults.set(notificationTitle, forKey: "notification_title") - - // Schedule the background services - BackgroundServicePlugin.scheduleBackgroundSync() - BackgroundServicePlugin.scheduleBackgroundFetch() - - result(true) - } - - // Called by the flutter code at launch to see if the background service is enabled or not - func handleIsEnabled(call: FlutterMethodCall, result: FlutterResult) { - let defaults = UserDefaults.standard - let enabled = defaults.value(forKey: "background_service_enabled") as? Bool - - // False by default - result(enabled ?? false) - } - - // Called by the Flutter code whenever a change in configuration is set - func handleConfigure(call: FlutterMethodCall, result: FlutterResult) { - - // Needs to be able to parse the arguments or else fail - guard let args = call.arguments as? Array else { - print("Cannot parse args as array: \(call.arguments)") - result(FlutterError()) - return - } - - // Needs to have 4 arguments in the call or else fail - guard args.count == 4 else { - print("Not enough arguments, 4 required: \(args.count) given") - result(FlutterError()) - return - } - - // Parse the arguments from the method call - let requireUnmeteredNetwork = args[0] as? Bool - let requireCharging = args[1] as? Bool - let triggerUpdateDelay = args[2] as? Int - let triggerMaxDelay = args[3] as? Int - - // Store the values from the call in the defaults - let defaults = UserDefaults.standard - defaults.set(requireUnmeteredNetwork, forKey: "require_unmetered_network") - defaults.set(requireCharging, forKey: "require_charging") - defaults.set(triggerUpdateDelay, forKey: "trigger_update_delay") - defaults.set(triggerMaxDelay, forKey: "trigger_max_delay") - - // Cancel the background services and reschedule them - BGTaskScheduler.shared.cancelAllTaskRequests() - BackgroundServicePlugin.scheduleBackgroundSync() - BackgroundServicePlugin.scheduleBackgroundFetch() - result(true) - } - - // Returns the number of currently scheduled background processes to Flutter, strictly - // for debugging - func handleNumberOfProcesses(call: FlutterMethodCall, result: @escaping FlutterResult) { - BGTaskScheduler.shared.getPendingTaskRequests { requests in - result(requests.count) - } - } - - // Disables the service, cancels all the task requests - func handleDisable(call: FlutterMethodCall, result: FlutterResult) { - let defaults = UserDefaults.standard - defaults.set(false, forKey: "background_service_enabled") - - BGTaskScheduler.shared.cancelAllTaskRequests() - result(true) - } - - // Checks the status of the Background App Refresh from the system - // Returns true if the service is enabled for Immich, and false otherwise - func handleBackgroundRefreshStatus(call: FlutterMethodCall, result: FlutterResult) { - switch UIApplication.shared.backgroundRefreshStatus { - case .available: - result(true) - break - case .denied: - result(false) - break - case .restricted: - result(false) - break - default: - result(false) - break - } - } - - - // Schedules a short-running background sync to sync only a few photos - static func scheduleBackgroundFetch() { - // We will schedule this task to run no matter the charging or wifi requirents from the end user - // 1. They can set Background App Refresh to Off / Wi-Fi / Wi-Fi & Cellular Data from Settings - // 2. We will check the battery connectivity when we begin running the background activity - let backgroundFetch = BGAppRefreshTaskRequest(identifier: BackgroundServicePlugin.backgroundFetchTaskID) - - // Use 5 minutes from now as earliest begin date - backgroundFetch.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) - - do { - try BGTaskScheduler.shared.submit(backgroundFetch) - } catch { - print("Could not schedule the background task \(error.localizedDescription)") - } - } - - // Schedules a long-running background sync for syncing all of the photos - static func scheduleBackgroundSync() { - let backgroundProcessing = BGProcessingTaskRequest(identifier: BackgroundServicePlugin.backgroundProcessingTaskID) - - // We need the values for requiring charging - let defaults = UserDefaults.standard - let requireCharging = defaults.value(forKey: "require_charging") as? Bool - - // Always require network connectivity, and set the require charging from the above - backgroundProcessing.requiresNetworkConnectivity = true - backgroundProcessing.requiresExternalPower = requireCharging ?? true - - // Use 15 minutes from now as earliest begin date - backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) - - do { - // Submit the task to the scheduler - try BGTaskScheduler.shared.submit(backgroundProcessing) - } catch { - print("Could not schedule the background task \(error.localizedDescription)") - } - } - - // This function runs when the system kicks off the BGAppRefreshTask from the Background Task Scheduler - static func handleBackgroundFetch(task: BGAppRefreshTask) { - // Schedule the next sync task so we can run this again later - scheduleBackgroundFetch() - - // Log the time of last background processing to now - let defaults = UserDefaults.standard - defaults.set(Date().timeIntervalSince1970, forKey: "last_background_fetch_run_time") - - // If we have required charging, we should check the charging status - let requireCharging = defaults.value(forKey: "require_charging") as? Bool ?? false - if (requireCharging) { - UIDevice.current.isBatteryMonitoringEnabled = true - if (UIDevice.current.batteryState == .unplugged) { - // The device is unplugged and we have required charging - // Therefore, we will simply complete the task without - // running it. - task.setTaskCompleted(success: true) - return - } - } - - // If we have required Wi-Fi, we can check the isExpensive property - let requireWifi = defaults.value(forKey: "require_wifi") as? Bool ?? false - if (requireWifi) { - let wifiMonitor = NWPathMonitor(requiredInterfaceType: .wifi) - let isExpensive = wifiMonitor.currentPath.isExpensive - if (isExpensive) { - // The network is expensive and we have required Wi-Fi - // Therefore, we will simply complete the task without - // running it - task.setTaskCompleted(success: true) - return - } - } - - // Schedule the next sync task so we can run this again later - scheduleBackgroundFetch() - - // The background sync task should only run for 20 seconds at most - BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: 20) - } - - // This function runs when the system kicks off the BGProcessingTask from the Background Task Scheduler - static func handleBackgroundProcessing(task: BGProcessingTask) { - // Schedule the next sync task so we run this again later - scheduleBackgroundSync() - - // Log the time of last background processing to now - let defaults = UserDefaults.standard - defaults.set(Date().timeIntervalSince1970, forKey: "last_background_processing_run_time") - - // We won't specify a max time for the background sync service, so this can run for longer - BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: nil) - } - - // This is a synchronous function which uses a semaphore to run the background sync worker's run - // function, which will create a background Isolate and communicate with the Flutter code to back - // up the assets. When it completes, we signal the semaphore and complete the execution allowing the - // control to pass back to the caller synchronously - static func runBackgroundSync(_ task: BGTask, maxSeconds: Int?) { - - let semaphore = DispatchSemaphore(value: 0) - DispatchQueue.main.async { - let backgroundWorker = BackgroundSyncWorker { _ in - semaphore.signal() - } - task.expirationHandler = { - backgroundWorker.cancel() - task.setTaskCompleted(success: true) - } - - backgroundWorker.run(maxSeconds: maxSeconds) - task.setTaskCompleted(success: true) - } - semaphore.wait() - } - - -} diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift b/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift deleted file mode 100644 index 88d9368308..0000000000 --- a/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift +++ /dev/null @@ -1,271 +0,0 @@ -// -// BackgroundSyncProcessing.swift -// Runner -// -// Created by Marty Fuhry on 2/6/23. -// -// Credit to https://github.com/fluttercommunity/flutter_workmanager/blob/main/ios/Classes/BackgroundWorker.swift - -import Foundation -import Flutter -import BackgroundTasks - -// The background worker which creates a new Flutter VM, communicates with it -// to run the backup job, and then finishes execution and calls back to its callback -// handler -class BackgroundSyncWorker { - - // The Flutter engine we create for background execution. - // This is not the main Flutter engine which shows the UI, - // this is a brand new isolate created and managed in this code - // here. It does not share memory with the main - // Flutter engine which shows the UI. - // It needs to be started up, registered, and torn down here - let engine: FlutterEngine? = FlutterEngine( - name: "BackgroundImmich" - ) - - let notificationId = "com.alextran.immich/backgroundNotifications" - // The background message passing channel - var channel: FlutterMethodChannel? - - var completionHandler: (UIBackgroundFetchResult) -> Void - let taskSessionStart = Date() - - // We need the completion handler to tell the system when we are done running - init(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - - // This is the background message passing channel to be used with the background engine - // created here in this platform code - self.channel = FlutterMethodChannel( - name: "immich/backgroundChannel", - binaryMessenger: engine!.binaryMessenger - ) - self.completionHandler = completionHandler - } - - // Handles all of the messages from the Flutter VM called into this platform code - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "initialized": - // Initialize tells us that we can now call into the Flutter VM to tell it to begin the update - self.channel?.invokeMethod( - "backgroundProcessing", - arguments: nil, - result: { flutterResult in - - // This is the result we send back to the BGTaskScheduler to let it know whether we'll need more time later or - // if this execution failed - let result: UIBackgroundFetchResult = (flutterResult as? Bool ?? false) ? .newData : .failed - - // Show the task duration - let taskSessionCompleter = Date() - let taskDuration = taskSessionCompleter.timeIntervalSince(self.taskSessionStart) - print("[\(String(describing: self))] \(#function) -> performBackgroundRequest.\(result) (finished in \(taskDuration) seconds)") - - // Complete the execution - self.complete(result) - }) - break - case "updateNotification": - let handled = self.handleNotification(call) - result(handled) - break - case "showError": - let handled = self.handleError(call) - result(handled) - break - case "clearErrorNotifications": - self.handleClearErrorNotifications() - result(true) - break - case "hasContentChanged": - // This is only called for Android, but we provide an implementation here - // telling Flutter that we don't have any information about whether the gallery - // contents have changed or not, so we can just say "no, they've not changed" - result(false) - break - default: - result(FlutterError()) - self.complete(UIBackgroundFetchResult.failed) - } - } - - // Runs the background sync by starting up a new isolate and handling the calls - // until it completes - public func run(maxSeconds: Int?) { - // We need the callback handle to start up the Flutter VM from the entry point - let defaults = UserDefaults.standard - guard let callbackHandle = defaults.value(forKey: "callback_handle") as? Int64 else { - // Can't find the callback handle, this is fatal - complete(UIBackgroundFetchResult.failed) - return - - } - - // Use the provided callbackHandle to get the callback function - guard let callback = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else { - // We need this callback or else this is fatal - complete(UIBackgroundFetchResult.failed) - return - } - - // Sanity check for the engine existing - if engine == nil { - complete(UIBackgroundFetchResult.failed) - return - } - - // Run the engine - let isRunning = engine!.run( - withEntrypoint: callback.callbackName, - libraryURI: callback.callbackLibraryPath - ) - - // If this engine isn't running, this is fatal - if !isRunning { - complete(UIBackgroundFetchResult.failed) - return - } - - // If we have a timer, we need to start the timer to cancel ourselves - // so that we don't run longer than the provided maxSeconds - // After maxSeconds has elapsed, we will invoke "systemStop" - if maxSeconds != nil { - // Schedule a non-repeating timer to run after maxSeconds - let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), - repeats: false) { timer in - // The callback invalidates the timer and stops execution - timer.invalidate() - - // If the channel is already deallocated, we don't need to do anything - if self.channel == nil { - return - } - - // Tell the Flutter VM to stop backing up now - self.channel?.invokeMethod( - "systemStop", - arguments: nil, - result: nil) - - // Complete the execution - self.complete(UIBackgroundFetchResult.newData) - } - } - - // Set the handle function to the channel message handler - self.channel?.setMethodCallHandler(handle) - - // Register this to get access to the plugins on the platform channel - BackgroundServicePlugin.flutterPluginRegistrantCallback?(engine!) - } - - // Cancels execution of this task, used by the system's task expiration handler - // which is called shortly before execution is about to expire - public func cancel() { - // If the channel is already deallocated, we don't need to do anything - if self.channel == nil { - return - } - - // Tell the Flutter VM to stop backing up now - self.channel?.invokeMethod( - "systemStop", - arguments: nil, - result: nil) - - // Complete the execution - self.complete(UIBackgroundFetchResult.newData) - } - - // Completes the execution, destroys the engine, and sends a completion to our callback completionHandler - private func complete(_ fetchResult: UIBackgroundFetchResult) { - engine?.destroyContext() - channel = nil - completionHandler(fetchResult) - } - - private func handleNotification(_ call: FlutterMethodCall) -> Bool { - - // Parse the arguments as an array list - guard let args = call.arguments as? Array else { - print("Failed to parse \(call.arguments) as array") - return false; - } - - // Requires 7 arguments passed or else fail - guard args.count == 7 else { - print("Needs 7 arguments, but was only passed \(args.count)") - return false - } - - // Parse the arguments to send the notification update - let title = args[0] as? String - let content = args[1] as? String - let progress = args[2] as? Int - let maximum = args[3] as? Int - let indeterminate = args[4] as? Bool - let isDetail = args[5] as? Bool - let onlyIfForeground = args[6] as? Bool - - // Build the notification - let notificationContent = UNMutableNotificationContent() - notificationContent.body = content ?? "Uploading..." - notificationContent.title = title ?? "Immich" - - // Add it to the Notification center - let notification = UNNotificationRequest( - identifier: notificationId, - content: notificationContent, - trigger: nil - ) - let center = UNUserNotificationCenter.current() - center.add(notification) { (error: Error?) in - if let theError = error { - print("Error showing notifications: \(theError)") - } - } - - return true - } - - private func handleError(_ call: FlutterMethodCall) -> Bool { - // Parse the arguments as an array list - guard let args = call.arguments as? Array else { - return false; - } - - // Requires 7 arguments passed or else fail - guard args.count == 3 else { - return false - } - - let title = args[0] as? String - let content = args[1] as? String - let individualTag = args[2] as? String - - // Build the notification - let notificationContent = UNMutableNotificationContent() - notificationContent.body = content ?? "Error running the backup job." - notificationContent.title = title ?? "Immich" - - // Add it to the Notification center - let notification = UNNotificationRequest( - identifier: notificationId, - content: notificationContent, - trigger: nil - ) - let center = UNUserNotificationCenter.current() - center.add(notification) - - return true - } - - private func handleClearErrorNotifications() { - let center = UNUserNotificationCenter.current() - center.removeDeliveredNotifications(withIdentifiers: [notificationId]) - center.removePendingNotificationRequests(withIdentifiers: [notificationId]) - } -} - diff --git a/mobile/ios/Runner/Connectivity/Connectivity.g.swift b/mobile/ios/Runner/Connectivity/Connectivity.g.swift index f8d85a2edf..c7aff63e10 100644 --- a/mobile/ios/Runner/Connectivity/Connectivity.g.swift +++ b/mobile/ios/Runner/Connectivity/Connectivity.g.swift @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] { } return [ "\(error)", - "\(type(of: error))", + "\(Swift.type(of: error))", "Stacktrace: \(Thread.callStackSymbols)", ] } diff --git a/mobile/ios/Runner/Core/Network.g.swift b/mobile/ios/Runner/Core/Network.g.swift index 5a8075f91a..7d9b9f14be 100644 --- a/mobile/ios/Runner/Core/Network.g.swift +++ b/mobile/ios/Runner/Core/Network.g.swift @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] { } return [ "\(error)", - "\(type(of: error))", + "\(Swift.type(of: error))", "Stacktrace: \(Thread.callStackSymbols)", ] } @@ -46,6 +46,19 @@ private func nilOrValue(_ value: Any?) -> T? { return value as! T? } +private func doubleEqualsNetwork(_ lhs: Double, _ rhs: Double) -> Bool { + return (lhs.isNaN && rhs.isNaN) || lhs == rhs +} + +private func doubleHashNetwork(_ value: Double, _ hasher: inout Hasher) { + if value.isNaN { + hasher.combine(0x7FF8000000000000) + } else { + // Normalize -0.0 to 0.0 + hasher.combine(value == 0 ? 0 : value) + } +} + func deepEqualsNetwork(_ lhs: Any?, _ rhs: Any?) -> Bool { let cleanLhs = nilOrValue(lhs) as Any? let cleanRhs = nilOrValue(rhs) as Any? @@ -56,59 +69,92 @@ func deepEqualsNetwork(_ lhs: Any?, _ rhs: Any?) -> Bool { case (nil, _), (_, nil): return false + case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: + return true + 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 !deepEqualsNetwork(element, cleanRhsArray[index]) { + case (let lhsArray, let rhsArray) as ([Any?], [Any?]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !deepEqualsNetwork(element, rhsArray[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 !deepEqualsNetwork(cleanLhsValue, cleanRhsDictionary[key]!) { + case (let lhsArray, let rhsArray) as ([Double], [Double]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !doubleEqualsNetwork(element, rhsArray[index]) { return false } } return true + case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard lhsDictionary.count == rhsDictionary.count else { return false } + for (lhsKey, lhsValue) in lhsDictionary { + var found = false + for (rhsKey, rhsValue) in rhsDictionary { + if deepEqualsNetwork(lhsKey, rhsKey) { + if deepEqualsNetwork(lhsValue, rhsValue) { + found = true + break + } else { + return false + } + } + } + if !found { return false } + } + return true + + case (let lhs as Double, let rhs as Double): + return doubleEqualsNetwork(lhs, rhs) + + case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): + return lhsHashable == rhsHashable + 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 deepHashNetwork(value: Any?, hasher: inout Hasher) { - if let valueList = value as? [AnyHashable] { - for item in valueList { deepHashNetwork(value: item, hasher: &hasher) } - return - } - - if let valueDict = value as? [AnyHashable: AnyHashable] { - for key in valueDict.keys { - hasher.combine(key) - deepHashNetwork(value: valueDict[key]!, hasher: &hasher) + let cleanValue = nilOrValue(value) as Any? + if let cleanValue = cleanValue { + if let doubleValue = cleanValue as? Double { + doubleHashNetwork(doubleValue, &hasher) + } else if let valueList = cleanValue as? [Any?] { + for item in valueList { + deepHashNetwork(value: item, hasher: &hasher) + } + } else if let valueList = cleanValue as? [Double] { + for item in valueList { + doubleHashNetwork(item, &hasher) + } + } else if let valueDict = cleanValue as? [AnyHashable: Any?] { + var result = 0 + for (key, value) in valueDict { + var entryKeyHasher = Hasher() + deepHashNetwork(value: key, hasher: &entryKeyHasher) + var entryValueHasher = Hasher() + deepHashNetwork(value: value, hasher: &entryValueHasher) + result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) + } + hasher.combine(result) + } else if let hashableValue = cleanValue as? AnyHashable { + hasher.combine(hashableValue) + } else { + hasher.combine(String(describing: cleanValue)) } - return + } else { + hasher.combine(0) } - - 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 ClientCertData: Hashable { @@ -133,9 +179,16 @@ struct ClientCertData: Hashable { ] } static func == (lhs: ClientCertData, rhs: ClientCertData) -> Bool { - return deepEqualsNetwork(lhs.toList(), rhs.toList()) } + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsNetwork(lhs.data, rhs.data) && deepEqualsNetwork(lhs.password, rhs.password) + } + func hash(into hasher: inout Hasher) { - deepHashNetwork(value: toList(), hasher: &hasher) + hasher.combine("ClientCertData") + deepHashNetwork(value: data, hasher: &hasher) + deepHashNetwork(value: password, hasher: &hasher) } } @@ -170,9 +223,18 @@ struct ClientCertPrompt: Hashable { ] } static func == (lhs: ClientCertPrompt, rhs: ClientCertPrompt) -> Bool { - return deepEqualsNetwork(lhs.toList(), rhs.toList()) } + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsNetwork(lhs.title, rhs.title) && deepEqualsNetwork(lhs.message, rhs.message) && deepEqualsNetwork(lhs.cancel, rhs.cancel) && deepEqualsNetwork(lhs.confirm, rhs.confirm) + } + func hash(into hasher: inout Hasher) { - deepHashNetwork(value: toList(), hasher: &hasher) + hasher.combine("ClientCertPrompt") + deepHashNetwork(value: title, hasher: &hasher) + deepHashNetwork(value: message, hasher: &hasher) + deepHashNetwork(value: cancel, hasher: &hasher) + deepHashNetwork(value: confirm, hasher: &hasher) } } diff --git a/mobile/ios/Runner/Core/NetworkApiImpl.swift b/mobile/ios/Runner/Core/NetworkApiImpl.swift index 3c4be8e718..82a913d837 100644 --- a/mobile/ios/Runner/Core/NetworkApiImpl.swift +++ b/mobile/ios/Runner/Core/NetworkApiImpl.swift @@ -10,11 +10,14 @@ enum ImportError: Error { } class NetworkApiImpl: NetworkApi { - weak var viewController: UIViewController? private var activeImporter: CertImporter? - - init(viewController: UIViewController?) { - self.viewController = viewController + + private var viewController: UIViewController? { + UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .flatMap { $0.windows } + .first { $0.isKeyWindow }? + .rootViewController } func selectCertificate(promptText: ClientCertPrompt, completion: @escaping (Result) -> Void) { diff --git a/mobile/ios/Runner/Core/URLSessionManager.swift b/mobile/ios/Runner/Core/URLSessionManager.swift index 0b73ed71a6..e9d65d3113 100644 --- a/mobile/ios/Runner/Core/URLSessionManager.swift +++ b/mobile/ios/Runner/Core/URLSessionManager.swift @@ -36,7 +36,7 @@ extension UserDefaults { /// Old sessions are kept alive by Dart's FFI retain until all isolates release them. class URLSessionManager: NSObject { static let shared = URLSessionManager() - + private(set) var session: URLSession let delegate: URLSessionManagerDelegate private static let cacheDir: URL = { @@ -53,7 +53,7 @@ class URLSessionManager: NSObject { ) static let userAgent: String = { let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown" - return "Immich_iOS_\(version)" + return "immich-ios/\(version)" }() static let cookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: APP_GROUP) private static var serverUrls: [String] = [] @@ -150,7 +150,6 @@ class URLSessionManager: NSObject { config.httpCookieStorage = cookieStorage config.httpMaximumConnectionsPerHost = 64 config.timeoutIntervalForRequest = 60 - config.timeoutIntervalForResource = 300 var headers = UserDefaults.group.dictionary(forKey: HEADERS_KEY) as? [String: String] ?? [:] headers["User-Agent"] = headers["User-Agent"] ?? userAgent diff --git a/mobile/ios/Runner/Images/ImageProcessing.swift b/mobile/ios/Runner/Images/ImageProcessing.swift index 2270bbffac..686a3464a7 100644 --- a/mobile/ios/Runner/Images/ImageProcessing.swift +++ b/mobile/ios/Runner/Images/ImageProcessing.swift @@ -1,7 +1,12 @@ import Foundation enum ImageProcessing { - static let queue = DispatchQueue(label: "thumbnail.processing", qos: .userInitiated, attributes: .concurrent) - static let semaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount * 2) + static let queue = { + let q = OperationQueue() + q.name = "thumbnail.processing" + q.qualityOfService = .userInitiated + q.maxConcurrentOperationCount = ProcessInfo.processInfo.activeProcessorCount * 2 + return q + }() static let cancelledResult = Result<[String: Int64]?, any Error>.success(nil) } diff --git a/mobile/ios/Runner/Images/ImageRequest.swift b/mobile/ios/Runner/Images/ImageRequest.swift new file mode 100644 index 0000000000..6c8bb04c70 --- /dev/null +++ b/mobile/ios/Runner/Images/ImageRequest.swift @@ -0,0 +1,41 @@ +import Foundation + +class ImageRequest: @unchecked Sendable { + private struct State: Sendable { + var isCancelled = false + } + + let completion: @Sendable (Result<[String: Int64]?, any Error>) -> Void + private let state: Mutex + + var isCancelled: Bool { + get { + state.withLock { $0.isCancelled } + } + set { + state.withLock { $0.isCancelled = newValue } + } + } + + init(completion: @escaping @Sendable (Result<[String: Int64]?, any Error>) -> Void) { + self.state = Mutex(State()) + self.completion = completion + } + + func cancel() { + isCancelled = true + } +} + +struct RequestRegistry: ~Copyable, Sendable { + private let requests = Mutex<[Int64: T]>([:]) + + func add(requestId: Int64, request: T) { + requests.withLock { $0[requestId] = request } + } + + @discardableResult + func remove(requestId: Int64) -> T? { + requests.withLock { $0.removeValue(forKey: requestId) } + } +} diff --git a/mobile/ios/Runner/Images/LocalImages.g.swift b/mobile/ios/Runner/Images/LocalImages.g.swift index 146950cd51..b9324260be 100644 --- a/mobile/ios/Runner/Images/LocalImages.g.swift +++ b/mobile/ios/Runner/Images/LocalImages.g.swift @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] { } return [ "\(error)", - "\(type(of: error))", + "\(Swift.type(of: error))", "Stacktrace: \(Thread.callStackSymbols)", ] } diff --git a/mobile/ios/Runner/Images/LocalImagesImpl.swift b/mobile/ios/Runner/Images/LocalImagesImpl.swift index 303ff5bc33..9c142da054 100644 --- a/mobile/ios/Runner/Images/LocalImagesImpl.swift +++ b/mobile/ios/Runner/Images/LocalImagesImpl.swift @@ -3,16 +3,6 @@ import Flutter import MobileCoreServices import Photos -class LocalImageRequest { - weak var workItem: DispatchWorkItem? - var isCancelled = false - let callback: (Result<[String: Int64]?, any Error>) -> Void - - init(callback: @escaping (Result<[String: Int64]?, any Error>) -> Void) { - self.callback = callback - } -} - class LocalImageApiImpl: LocalImageApi { private static let imageManager = PHImageManager.default() private static let fetchOptions = { @@ -31,18 +21,15 @@ class LocalImageApiImpl: LocalImageApi { return requestOptions }() - private static let assetQueue = DispatchQueue(label: "thumbnail.assets", qos: .userInitiated) - private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated) - private static let cancelQueue = DispatchQueue(label: "thumbnail.cancellation", qos: .default) + private static let registry = RequestRegistry() - private static var rgbaFormat = vImage_CGImageFormat( + private static let rgbaFormat = vImage_CGImageFormat( bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue), renderingIntent: .defaultIntent )! - private static var requests = [Int64: LocalImageRequest]() private static let assetCache = { let assetCache = NSCache() assetCache.countLimit = 10000 @@ -50,7 +37,7 @@ class LocalImageApiImpl: LocalImageApi { }() func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) { - ImageProcessing.queue.async { + ImageProcessing.queue.addOperation { guard let data = Data(base64Encoded: thumbhash) else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))} @@ -65,30 +52,20 @@ class LocalImageApiImpl: LocalImageApi { } func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) { - let request = LocalImageRequest(callback: completion) - let item = DispatchWorkItem { + let request = ImageRequest(completion: completion) + let operation = BlockOperation { if request.isCancelled { - return completion(ImageProcessing.cancelledResult) - } - - ImageProcessing.semaphore.wait() - defer { - ImageProcessing.semaphore.signal() - } - - if request.isCancelled { - return completion(ImageProcessing.cancelledResult) + return request.completion(ImageProcessing.cancelledResult) } guard let asset = Self.requestAsset(assetId: assetId) else { - Self.remove(requestId: requestId) - completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil))) - return + Self.registry.remove(requestId: requestId) + return request.completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil))) } if request.isCancelled { - return completion(ImageProcessing.cancelledResult) + return request.completion(ImageProcessing.cancelledResult) } if preferEncoded { @@ -107,13 +84,12 @@ class LocalImageApiImpl: LocalImageApi { ) if request.isCancelled { - Self.remove(requestId: requestId) - return completion(ImageProcessing.cancelledResult) + return request.completion(ImageProcessing.cancelledResult) } guard let data = imageData else { - Self.remove(requestId: requestId) - return completion(.failure(PigeonError(code: "", message: "Could not get image data for \(assetId)", details: nil))) + Self.registry.remove(requestId: requestId) + return request.completion(.failure(PigeonError(code: "", message: "Could not get image data for \(assetId)", details: nil))) } let length = data.count @@ -122,16 +98,14 @@ class LocalImageApiImpl: LocalImageApi { if request.isCancelled { free(pointer) - Self.remove(requestId: requestId) - return completion(ImageProcessing.cancelledResult) + return request.completion(ImageProcessing.cancelledResult) } - request.callback(.success([ + Self.registry.remove(requestId: requestId) + return request.completion(.success([ "pointer": Int64(Int(bitPattern: pointer)), "length": Int64(length), ])) - Self.remove(requestId: requestId) - return } var image: UIImage? @@ -146,17 +120,17 @@ class LocalImageApiImpl: LocalImageApi { ) if request.isCancelled { - return completion(ImageProcessing.cancelledResult) + return request.completion(ImageProcessing.cancelledResult) } guard let image = image, let cgImage = image.cgImage else { - Self.remove(requestId: requestId) - return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil))) + Self.registry.remove(requestId: requestId) + return request.completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil))) } if request.isCancelled { - return completion(ImageProcessing.cancelledResult) + return request.completion(ImageProcessing.cancelledResult) } do { @@ -164,58 +138,38 @@ class LocalImageApiImpl: LocalImageApi { if request.isCancelled { buffer.free() - return completion(ImageProcessing.cancelledResult) + return request.completion(ImageProcessing.cancelledResult) } - request.callback(.success([ + Self.registry.remove(requestId: requestId) + return request.completion(.success([ "pointer": Int64(Int(bitPattern: buffer.data)), "width": Int64(buffer.width), "height": Int64(buffer.height), - "rowBytes": Int64(buffer.rowBytes) + "rowBytes": Int64(buffer.rowBytes), ])) - Self.remove(requestId: requestId) } catch { - Self.remove(requestId: requestId) - return completion(.failure(PigeonError(code: "", message: "Failed to convert image for \(assetId): \(error)", details: nil))) + Self.registry.remove(requestId: requestId) + return request.completion(.failure(PigeonError(code: "", message: "Failed to convert image for \(assetId): \(error)", details: nil))) } } - request.workItem = item - Self.add(requestId: requestId, request: request) - ImageProcessing.queue.async(execute: item) + Self.registry.add(requestId: requestId, request: request) + ImageProcessing.queue.addOperation(operation) } func cancelRequest(requestId: Int64) { - Self.cancel(requestId: requestId) - } - - private static func add(requestId: Int64, request: LocalImageRequest) -> Void { - requestQueue.sync { requests[requestId] = request } - } - - private static func remove(requestId: Int64) -> Void { - requestQueue.sync { requests[requestId] = nil } - } - - private static func cancel(requestId: Int64) -> Void { - requestQueue.async { - guard let request = requests.removeValue(forKey: requestId) else { return } - request.isCancelled = true - guard let item = request.workItem else { return } - if item.isCancelled { - cancelQueue.async { request.callback(ImageProcessing.cancelledResult) } - } - } + Self.registry.remove(requestId: requestId)?.cancel() } private static func requestAsset(assetId: String) -> PHAsset? { - var asset: PHAsset? - assetQueue.sync { asset = assetCache.object(forKey: assetId as NSString) } - if asset != nil { return asset } + if let cached = assetCache.object(forKey: assetId as NSString) { + return cached + } guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject else { return nil } - assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) } + assetCache.setObject(asset, forKey: assetId as NSString) return asset } } diff --git a/mobile/ios/Runner/Images/RemoteImages.g.swift b/mobile/ios/Runner/Images/RemoteImages.g.swift index 9fcffd4233..12eaaeec60 100644 --- a/mobile/ios/Runner/Images/RemoteImages.g.swift +++ b/mobile/ios/Runner/Images/RemoteImages.g.swift @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] { } return [ "\(error)", - "\(type(of: error))", + "\(Swift.type(of: error))", "Stacktrace: \(Thread.callStackSymbols)", ] } diff --git a/mobile/ios/Runner/Images/RemoteImagesImpl.swift b/mobile/ios/Runner/Images/RemoteImagesImpl.swift index f2a0c37254..de1f6dec89 100644 --- a/mobile/ios/Runner/Images/RemoteImagesImpl.swift +++ b/mobile/ios/Runner/Images/RemoteImagesImpl.swift @@ -3,23 +3,24 @@ import Flutter import MobileCoreServices import Photos -class RemoteImageRequest { - weak var task: URLSessionDataTask? +final class RemoteImageRequest: ImageRequest { + var task: URLSessionDataTask? let id: Int64 - var isCancelled = false - let completion: (Result<[String: Int64]?, any Error>) -> Void - init(id: Int64, task: URLSessionDataTask, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) { + init(id: Int64, completion: @escaping @Sendable (Result<[String: Int64]?, any Error>) -> Void) { self.id = id - self.task = task - self.completion = completion + super.init(completion: completion) + } + + override func cancel() { + super.cancel() + task?.cancel() } } class RemoteImageApiImpl: NSObject, RemoteImageApi { - private static var lock = os_unfair_lock() - private static var requests = [Int64: RemoteImageRequest]() - private static var rgbaFormat = vImage_CGImageFormat( + private static let registry = RequestRegistry() + private static let rgbaFormat = vImage_CGImageFormat( bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: CGColorSpaceCreateDeviceRGB(), @@ -37,70 +38,58 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi { var urlRequest = URLRequest(url: URL(string: url)!) urlRequest.cachePolicy = .returnCacheDataElseLoad + let request = RemoteImageRequest(id: requestId, completion: completion) + let task = URLSessionManager.shared.session.dataTask(with: urlRequest) { data, response, error in - Self.handleCompletion(requestId: requestId, encoded: preferEncoded, data: data, response: response, error: error) + Self.handleCompletion(request: request, encoded: preferEncoded, data: data, response: response, error: error) } - let request = RemoteImageRequest(id: requestId, task: task, completion: completion) - - os_unfair_lock_lock(&Self.lock) - Self.requests[requestId] = request - os_unfair_lock_unlock(&Self.lock) - + request.task = task + Self.registry.add(requestId: requestId, request: request) task.resume() } - private static func handleCompletion(requestId: Int64, encoded: Bool, data: Data?, response: URLResponse?, error: Error?) { - os_unfair_lock_lock(&Self.lock) - guard let request = requests[requestId] else { - return os_unfair_lock_unlock(&Self.lock) - } - requests[requestId] = nil - os_unfair_lock_unlock(&Self.lock) - - if let error = error { - if request.isCancelled || (error as NSError).code == NSURLErrorCancelled { - return request.completion(ImageProcessing.cancelledResult) - } - return request.completion(.failure(error)) - } - + private static func handleCompletion(request: RemoteImageRequest, encoded: Bool, data: Data?, response: URLResponse?, error: Error?) { if request.isCancelled { return request.completion(ImageProcessing.cancelledResult) } + if let error = error { + registry.remove(requestId: request.id) + return request.completion(.failure(error)) + } + guard let data = data else { + registry.remove(requestId: request.id) return request.completion(.failure(PigeonError(code: "", message: "No data received", details: nil))) } - ImageProcessing.queue.async { - ImageProcessing.semaphore.wait() - defer { ImageProcessing.semaphore.signal() } + if encoded { + let length = data.count + let pointer = malloc(length)! + data.copyBytes(to: pointer.assumingMemoryBound(to: UInt8.self), count: length) + if request.isCancelled { + free(pointer) + return request.completion(ImageProcessing.cancelledResult) + } + + registry.remove(requestId: request.id) + return request.completion( + .success([ + "pointer": Int64(Int(bitPattern: pointer)), + "length": Int64(length), + ])) + } + + ImageProcessing.queue.addOperation { if request.isCancelled { return request.completion(ImageProcessing.cancelledResult) } - // Return raw encoded bytes when requested (for animated images) - if encoded { - let length = data.count - let pointer = malloc(length)! - data.copyBytes(to: pointer.assumingMemoryBound(to: UInt8.self), count: length) - - if request.isCancelled { - free(pointer) - return request.completion(ImageProcessing.cancelledResult) - } - - return request.completion( - .success([ - "pointer": Int64(Int(bitPattern: pointer)), - "length": Int64(length), - ])) - } - guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions) else { + registry.remove(requestId: request.id) return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil))) } @@ -116,27 +105,23 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi { return request.completion(ImageProcessing.cancelledResult) } - request.completion( - .success([ - "pointer": Int64(Int(bitPattern: buffer.data)), - "width": Int64(buffer.width), - "height": Int64(buffer.height), - "rowBytes": Int64(buffer.rowBytes), - ])) + registry.remove(requestId: request.id) + return request.completion( + .success([ + "pointer": Int64(Int(bitPattern: buffer.data)), + "width": Int64(buffer.width), + "height": Int64(buffer.height), + "rowBytes": Int64(buffer.rowBytes), + ])) } catch { + registry.remove(requestId: request.id) return request.completion(.failure(PigeonError(code: "", message: "Failed to convert image for request: \(error)", details: nil))) } } } func cancelRequest(requestId: Int64) { - os_unfair_lock_lock(&Self.lock) - let request = Self.requests[requestId] - os_unfair_lock_unlock(&Self.lock) - - guard let request = request else { return } - request.isCancelled = true - request.task?.cancel() + Self.registry.remove(requestId: requestId)?.cancel() } func clearCache(completion: @escaping (Result) -> Void) { diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index ab53ec0f8c..3b030e4f86 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -8,8 +8,6 @@ app.alextran.immich.background.refreshUpload app.alextran.immich.background.processingUpload - app.alextran.immich.backgroundFetch - app.alextran.immich.backgroundProcessing CADisableMinimumFrameDurationOnPhone @@ -80,7 +78,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.6.0 + 2.7.5 CFBundleSignature ???? CFBundleURLTypes @@ -108,8 +106,6 @@ CFBundleVersion 240 - FLTEnableImpeller - ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes @@ -154,6 +150,27 @@ INSendMessageIntent + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + UIApplicationSupportsIndirectInputEvents UIBackgroundModes diff --git a/mobile/ios/Runner/Sync/Messages.g.swift b/mobile/ios/Runner/Sync/Messages.g.swift index 6bba25d94b..bf7940226e 100644 --- a/mobile/ios/Runner/Sync/Messages.g.swift +++ b/mobile/ios/Runner/Sync/Messages.g.swift @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -50,7 +50,7 @@ private func wrapError(_ error: Any) -> [Any?] { } return [ "\(error)", - "\(type(of: error))", + "\(Swift.type(of: error))", "Stacktrace: \(Thread.callStackSymbols)", ] } @@ -64,6 +64,19 @@ private func nilOrValue(_ value: Any?) -> T? { return value as! T? } +private func doubleEqualsMessages(_ lhs: Double, _ rhs: Double) -> Bool { + return (lhs.isNaN && rhs.isNaN) || lhs == rhs +} + +private func doubleHashMessages(_ value: Double, _ hasher: inout Hasher) { + if value.isNaN { + hasher.combine(0x7FF8000000000000) + } else { + // Normalize -0.0 to 0.0 + hasher.combine(value == 0 ? 0 : value) + } +} + func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool { let cleanLhs = nilOrValue(lhs) as Any? let cleanRhs = nilOrValue(rhs) as Any? @@ -74,59 +87,92 @@ func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool { case (nil, _), (_, nil): return false + case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: + return true + case is (Void, Void): return true - case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): - return cleanLhsHashable == cleanRhsHashable - - case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): - guard cleanLhsArray.count == cleanRhsArray.count else { return false } - for (index, element) in cleanLhsArray.enumerated() { - if !deepEqualsMessages(element, cleanRhsArray[index]) { + case (let lhsArray, let rhsArray) as ([Any?], [Any?]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !deepEqualsMessages(element, rhsArray[index]) { return false } } return true - case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): - guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } - for (key, cleanLhsValue) in cleanLhsDictionary { - guard cleanRhsDictionary.index(forKey: key) != nil else { return false } - if !deepEqualsMessages(cleanLhsValue, cleanRhsDictionary[key]!) { + case (let lhsArray, let rhsArray) as ([Double], [Double]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !doubleEqualsMessages(element, rhsArray[index]) { return false } } return true + case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard lhsDictionary.count == rhsDictionary.count else { return false } + for (lhsKey, lhsValue) in lhsDictionary { + var found = false + for (rhsKey, rhsValue) in rhsDictionary { + if deepEqualsMessages(lhsKey, rhsKey) { + if deepEqualsMessages(lhsValue, rhsValue) { + found = true + break + } else { + return false + } + } + } + if !found { return false } + } + return true + + case (let lhs as Double, let rhs as Double): + return doubleEqualsMessages(lhs, rhs) + + case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): + return lhsHashable == rhsHashable + default: - // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue. return false } } func deepHashMessages(value: Any?, hasher: inout Hasher) { - if let valueList = value as? [AnyHashable] { - for item in valueList { deepHashMessages(value: item, hasher: &hasher) } - return - } - - if let valueDict = value as? [AnyHashable: AnyHashable] { - for key in valueDict.keys { - hasher.combine(key) - deepHashMessages(value: valueDict[key]!, hasher: &hasher) + let cleanValue = nilOrValue(value) as Any? + if let cleanValue = cleanValue { + if let doubleValue = cleanValue as? Double { + doubleHashMessages(doubleValue, &hasher) + } else if let valueList = cleanValue as? [Any?] { + for item in valueList { + deepHashMessages(value: item, hasher: &hasher) + } + } else if let valueList = cleanValue as? [Double] { + for item in valueList { + doubleHashMessages(item, &hasher) + } + } else if let valueDict = cleanValue as? [AnyHashable: Any?] { + var result = 0 + for (key, value) in valueDict { + var entryKeyHasher = Hasher() + deepHashMessages(value: key, hasher: &entryKeyHasher) + var entryValueHasher = Hasher() + deepHashMessages(value: value, hasher: &entryValueHasher) + result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) + } + hasher.combine(result) + } else if let hashableValue = cleanValue as? AnyHashable { + hasher.combine(hashableValue) + } else { + hasher.combine(String(describing: cleanValue)) } - return + } else { + hasher.combine(0) } - - if let hashableValue = value as? AnyHashable { - hasher.combine(hashableValue.hashValue) - } - - return hasher.combine(String(describing: value)) } - enum PlatformAssetPlaybackStyle: Int { case unknown = 0 @@ -208,9 +254,28 @@ struct PlatformAsset: Hashable { ] } static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool { - return deepEqualsMessages(lhs.toList(), rhs.toList()) } + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsMessages(lhs.id, rhs.id) && deepEqualsMessages(lhs.name, rhs.name) && deepEqualsMessages(lhs.type, rhs.type) && deepEqualsMessages(lhs.createdAt, rhs.createdAt) && deepEqualsMessages(lhs.updatedAt, rhs.updatedAt) && deepEqualsMessages(lhs.width, rhs.width) && deepEqualsMessages(lhs.height, rhs.height) && deepEqualsMessages(lhs.durationInSeconds, rhs.durationInSeconds) && deepEqualsMessages(lhs.orientation, rhs.orientation) && deepEqualsMessages(lhs.isFavorite, rhs.isFavorite) && deepEqualsMessages(lhs.adjustmentTime, rhs.adjustmentTime) && deepEqualsMessages(lhs.latitude, rhs.latitude) && deepEqualsMessages(lhs.longitude, rhs.longitude) && deepEqualsMessages(lhs.playbackStyle, rhs.playbackStyle) + } + func hash(into hasher: inout Hasher) { - deepHashMessages(value: toList(), hasher: &hasher) + hasher.combine("PlatformAsset") + deepHashMessages(value: id, hasher: &hasher) + deepHashMessages(value: name, hasher: &hasher) + deepHashMessages(value: type, hasher: &hasher) + deepHashMessages(value: createdAt, hasher: &hasher) + deepHashMessages(value: updatedAt, hasher: &hasher) + deepHashMessages(value: width, hasher: &hasher) + deepHashMessages(value: height, hasher: &hasher) + deepHashMessages(value: durationInSeconds, hasher: &hasher) + deepHashMessages(value: orientation, hasher: &hasher) + deepHashMessages(value: isFavorite, hasher: &hasher) + deepHashMessages(value: adjustmentTime, hasher: &hasher) + deepHashMessages(value: latitude, hasher: &hasher) + deepHashMessages(value: longitude, hasher: &hasher) + deepHashMessages(value: playbackStyle, hasher: &hasher) } } @@ -249,9 +314,19 @@ struct PlatformAlbum: Hashable { ] } static func == (lhs: PlatformAlbum, rhs: PlatformAlbum) -> Bool { - return deepEqualsMessages(lhs.toList(), rhs.toList()) } + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsMessages(lhs.id, rhs.id) && deepEqualsMessages(lhs.name, rhs.name) && deepEqualsMessages(lhs.updatedAt, rhs.updatedAt) && deepEqualsMessages(lhs.isCloud, rhs.isCloud) && deepEqualsMessages(lhs.assetCount, rhs.assetCount) + } + func hash(into hasher: inout Hasher) { - deepHashMessages(value: toList(), hasher: &hasher) + hasher.combine("PlatformAlbum") + deepHashMessages(value: id, hasher: &hasher) + deepHashMessages(value: name, hasher: &hasher) + deepHashMessages(value: updatedAt, hasher: &hasher) + deepHashMessages(value: isCloud, hasher: &hasher) + deepHashMessages(value: assetCount, hasher: &hasher) } } @@ -286,9 +361,18 @@ struct SyncDelta: Hashable { ] } static func == (lhs: SyncDelta, rhs: SyncDelta) -> Bool { - return deepEqualsMessages(lhs.toList(), rhs.toList()) } + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsMessages(lhs.hasChanges, rhs.hasChanges) && deepEqualsMessages(lhs.updates, rhs.updates) && deepEqualsMessages(lhs.deletes, rhs.deletes) && deepEqualsMessages(lhs.assetAlbums, rhs.assetAlbums) + } + func hash(into hasher: inout Hasher) { - deepHashMessages(value: toList(), hasher: &hasher) + hasher.combine("SyncDelta") + deepHashMessages(value: hasChanges, hasher: &hasher) + deepHashMessages(value: updates, hasher: &hasher) + deepHashMessages(value: deletes, hasher: &hasher) + deepHashMessages(value: assetAlbums, hasher: &hasher) } } @@ -319,9 +403,17 @@ struct HashResult: Hashable { ] } static func == (lhs: HashResult, rhs: HashResult) -> Bool { - return deepEqualsMessages(lhs.toList(), rhs.toList()) } + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsMessages(lhs.assetId, rhs.assetId) && deepEqualsMessages(lhs.error, rhs.error) && deepEqualsMessages(lhs.hash, rhs.hash) + } + func hash(into hasher: inout Hasher) { - deepHashMessages(value: toList(), hasher: &hasher) + hasher.combine("HashResult") + deepHashMessages(value: assetId, hasher: &hasher) + deepHashMessages(value: error, hasher: &hasher) + deepHashMessages(value: hash, hasher: &hasher) } } @@ -352,9 +444,17 @@ struct CloudIdResult: Hashable { ] } static func == (lhs: CloudIdResult, rhs: CloudIdResult) -> Bool { - return deepEqualsMessages(lhs.toList(), rhs.toList()) } + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsMessages(lhs.assetId, rhs.assetId) && deepEqualsMessages(lhs.error, rhs.error) && deepEqualsMessages(lhs.cloudId, rhs.cloudId) + } + func hash(into hasher: inout Hasher) { - deepHashMessages(value: toList(), hasher: &hasher) + hasher.combine("CloudIdResult") + deepHashMessages(value: assetId, hasher: &hasher) + deepHashMessages(value: error, hasher: &hasher) + deepHashMessages(value: cloudId, hasher: &hasher) } } diff --git a/mobile/ios/Runner/Utility/Mutex.swift b/mobile/ios/Runner/Utility/Mutex.swift new file mode 100644 index 0000000000..fbfe168ff4 --- /dev/null +++ b/mobile/ios/Runner/Utility/Mutex.swift @@ -0,0 +1,54 @@ +import Darwin + +// Can be replaced with std Mutex when the deployment target is iOS 18+ +struct Mutex: ~Copyable, @unchecked Sendable { + struct _Buffer: ~Copyable { + var lock: os_unfair_lock = .init() + var value: Value + + init(value: consuming Value) { + self.value = value + } + + deinit {} + } + + let _buffer: UnsafeMutablePointer<_Buffer> + + init(_ initialValue: consuming sending Value) { + _buffer = .allocate(capacity: 1) + _buffer.initialize(to: _Buffer(value: initialValue)) + } + + deinit { + _buffer.deinitialize(count: 1) + _buffer.deallocate() + } + + @discardableResult + borrowing func withLock( + _ body: (inout sending Value) throws(E) -> sending Result + ) throws(E) -> sending Result { + os_unfair_lock_lock(&_buffer.pointee.lock) + defer { os_unfair_lock_unlock(&_buffer.pointee.lock) } + return try body(&_buffer.pointee.value) + } +} + +// Can be replaced with OSAllocatedUnfairLock when the deployment target is iOS 16+ +typealias UnfairLock = Mutex + +extension Mutex where Value == Void { + init() { + self.init(()) + } + + @discardableResult + borrowing func withLock( + _ body: () throws(E) -> sending Result + ) throws(E) -> sending Result { + os_unfair_lock_lock(&_buffer.pointee.lock) + defer { os_unfair_lock_unlock(&_buffer.pointee.lock) } + return try body() + } +} diff --git a/mobile/lib/constants/aspect_ratios.dart b/mobile/lib/constants/aspect_ratios.dart new file mode 100644 index 0000000000..9159db4ef1 --- /dev/null +++ b/mobile/lib/constants/aspect_ratios.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +enum AspectRatioPreset { + free(ratio: null, label: 'Free', icon: Icons.crop_free_rounded), + square(ratio: 1.0, label: '1:1', icon: Icons.crop_square_rounded), + ratio16x9(ratio: 16 / 9, label: '16:9', icon: Icons.crop_16_9_rounded), + ratio3x2(ratio: 3 / 2, label: '3:2', icon: Icons.crop_3_2_rounded), + ratio7x5(ratio: 7 / 5, label: '7:5', icon: Icons.crop_7_5_rounded), + ratio9x16(ratio: 9 / 16, label: '9:16', icon: Icons.crop_16_9_rounded, iconRotated: true), + ratio2x3(ratio: 2 / 3, label: '2:3', icon: Icons.crop_3_2_rounded, iconRotated: true), + ratio5x7(ratio: 5 / 7, label: '5:7', icon: Icons.crop_7_5_rounded, iconRotated: true); + + final double? ratio; + final String label; + final IconData icon; + final bool iconRotated; + + const AspectRatioPreset({required this.ratio, required this.label, required this.icon, this.iconRotated = false}); +} diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index 9d28941b8f..1748a2a57d 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -1,9 +1,5 @@ import 'dart:io'; -const int noDbId = -9223372036854775808; // from Isar -const double downloadCompleted = -1; -const double downloadFailed = -2; - const String kMobileMetadataKey = "mobile-app"; // Number of log entries to retain on app start @@ -47,9 +43,6 @@ const List<(String, String)> kWidgetNames = [ ('com.immich.widget.memory', 'app.alextran.immich.widget.MemoryReceiver'), ]; -const double kUploadStatusFailed = -1.0; -const double kUploadStatusCanceled = -2.0; - const int kMinMonthsToEnableScrubberSnap = 12; const String kImmichAppStoreLink = "https://apps.apple.com/app/immich/id1613945652"; diff --git a/mobile/lib/constants/enums.dart b/mobile/lib/constants/enums.dart index 32ef9bbbed..877145c322 100644 --- a/mobile/lib/constants/enums.dart +++ b/mobile/lib/constants/enums.dart @@ -11,8 +11,6 @@ enum TextSearchType { context, filename, description, ocr } enum AssetVisibilityEnum { timeline, hidden, archive, locked } -enum SortUserBy { id } - enum ActionSource { timeline, viewer } enum CleanupStep { selectDate, scan, delete } diff --git a/mobile/lib/constants/locales.dart b/mobile/lib/constants/locales.dart index e20f037beb..f44aa5cc3e 100644 --- a/mobile/lib/constants/locales.dart +++ b/mobile/lib/constants/locales.dart @@ -7,7 +7,7 @@ const Map locales = { 'Arabic (ar)': Locale('ar'), 'Bulgarian (bg)': Locale('bg'), 'Catalan (ca)': Locale('ca'), - 'Chinese Simplified (zh_CN)': Locale.fromSubtags(languageCode: 'zh', scriptCode: 'SIMPLIFIED'), + 'Chinese Simplified (zh_CN)': Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'), 'Chinese Traditional (zh_TW)': Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), 'Croatian (hr)': Locale('hr'), 'Czech (cs)': Locale('cs'), diff --git a/mobile/lib/domain/interfaces/db.interface.dart b/mobile/lib/domain/interfaces/db.interface.dart deleted file mode 100644 index 5645d15c47..0000000000 --- a/mobile/lib/domain/interfaces/db.interface.dart +++ /dev/null @@ -1,3 +0,0 @@ -abstract interface class IDatabaseRepository { - Future transaction(Future Function() callback); -} diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart index cb40c8f76a..85c42fd24f 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -1,3 +1,5 @@ +import 'package:immich_mobile/domain/models/exif.model.dart'; + part 'local_asset.model.dart'; part 'remote_asset.model.dart'; @@ -69,6 +71,8 @@ sealed class BaseAsset { bool get isLocalOnly => storage == AssetState.local; bool get isRemoteOnly => storage == AssetState.remote; + bool get isEditable => false; + // Overridden in subclasses AssetState get storage; String? get localId; diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart index 43d49506e3..745e8f46ff 100644 --- a/mobile/lib/domain/models/asset/remote_asset.model.dart +++ b/mobile/lib/domain/models/asset/remote_asset.model.dart @@ -43,6 +43,9 @@ class RemoteAsset extends BaseAsset { @override String get heroTag => '${localId ?? checksum}_$id'; + @override + bool get isEditable => isImage && !isMotionPhoto && !isAnimatedImage; + @override String toString() { return '''Asset { @@ -128,3 +131,81 @@ class RemoteAsset extends BaseAsset { ); } } + +class RemoteAssetExif extends RemoteAsset { + final ExifInfo exifInfo; + + const RemoteAssetExif({ + required super.id, + super.localId, + required super.name, + required super.ownerId, + required super.checksum, + required super.type, + required super.createdAt, + required super.updatedAt, + super.width, + super.height, + super.durationInSeconds, + super.isFavorite = false, + super.thumbHash, + super.visibility = AssetVisibility.timeline, + super.livePhotoVideoId, + super.stackId, + super.isEdited = false, + this.exifInfo = const ExifInfo(), + }); + + @override + bool operator ==(Object other) { + if (other is! RemoteAssetExif) return false; + if (identical(this, other)) return true; + return super == other && exifInfo == other.exifInfo; + } + + @override + int get hashCode => super.hashCode ^ exifInfo.hashCode; + + @override + RemoteAssetExif copyWith({ + String? id, + String? localId, + String? name, + String? ownerId, + String? checksum, + AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + int? width, + int? height, + int? durationInSeconds, + bool? isFavorite, + String? thumbHash, + AssetVisibility? visibility, + String? livePhotoVideoId, + String? stackId, + bool? isEdited, + ExifInfo? exifInfo, + }) { + return RemoteAssetExif( + id: id ?? this.id, + localId: localId ?? this.localId, + name: name ?? this.name, + ownerId: ownerId ?? this.ownerId, + checksum: checksum ?? this.checksum, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + isFavorite: isFavorite ?? this.isFavorite, + thumbHash: thumbHash ?? this.thumbHash, + visibility: visibility ?? this.visibility, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + stackId: stackId ?? this.stackId, + isEdited: isEdited ?? this.isEdited, + exifInfo: exifInfo ?? this.exifInfo, // Use the new parameter + ); + } +} diff --git a/mobile/lib/domain/models/asset_edit.model.dart b/mobile/lib/domain/models/asset_edit.model.dart index b3266dba46..9809b9c606 100644 --- a/mobile/lib/domain/models/asset_edit.model.dart +++ b/mobile/lib/domain/models/asset_edit.model.dart @@ -1,21 +1,25 @@ -import "package:openapi/api.dart" as api show AssetEditAction; +import "package:openapi/api.dart" show CropParameters, RotateParameters, MirrorParameters; enum AssetEditAction { rotate, crop, mirror, other } -extension AssetEditActionExtension on AssetEditAction { - api.AssetEditAction? toDto() { - return switch (this) { - AssetEditAction.rotate => api.AssetEditAction.rotate, - AssetEditAction.crop => api.AssetEditAction.crop, - AssetEditAction.mirror => api.AssetEditAction.mirror, - AssetEditAction.other => null, - }; - } +sealed class AssetEdit { + const AssetEdit(); } -class AssetEdit { - final AssetEditAction action; - final Map parameters; +class CropEdit extends AssetEdit { + final CropParameters parameters; - const AssetEdit({required this.action, required this.parameters}); + const CropEdit(this.parameters); +} + +class RotateEdit extends AssetEdit { + final RotateParameters parameters; + + const RotateEdit(this.parameters); +} + +class MirrorEdit extends AssetEdit { + final MirrorParameters parameters; + + const MirrorEdit(this.parameters); } diff --git a/mobile/lib/domain/models/device_asset.model.dart b/mobile/lib/domain/models/device_asset.model.dart deleted file mode 100644 index a404f5a9e2..0000000000 --- a/mobile/lib/domain/models/device_asset.model.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:typed_data'; - -class DeviceAsset { - final String assetId; - final Uint8List hash; - final DateTime modifiedTime; - - const DeviceAsset({required this.assetId, required this.hash, required this.modifiedTime}); - - @override - bool operator ==(covariant DeviceAsset other) { - if (identical(this, other)) return true; - - return other.assetId == assetId && other.hash == hash && other.modifiedTime == modifiedTime; - } - - @override - int get hashCode { - return assetId.hashCode ^ hash.hashCode ^ modifiedTime.hashCode; - } - - @override - String toString() { - return 'DeviceAsset(assetId: $assetId, hash: $hash, modifiedTime: $modifiedTime)'; - } - - DeviceAsset copyWith({String? assetId, Uint8List? hash, DateTime? modifiedTime}) { - return DeviceAsset( - assetId: assetId ?? this.assetId, - hash: hash ?? this.hash, - modifiedTime: modifiedTime ?? this.modifiedTime, - ); - } -} diff --git a/mobile/lib/domain/models/exif.model.dart b/mobile/lib/domain/models/exif.model.dart index d0f78b59de..45b787d586 100644 --- a/mobile/lib/domain/models/exif.model.dart +++ b/mobile/lib/domain/models/exif.model.dart @@ -7,6 +7,8 @@ class ExifInfo { final String? timeZone; final DateTime? dateTimeOriginal; final int? rating; + final int? width; + final int? height; // GPS final double? latitude; @@ -48,6 +50,8 @@ class ExifInfo { this.timeZone, this.dateTimeOriginal, this.rating, + this.width, + this.height, this.isFlipped = false, this.latitude, this.longitude, @@ -74,6 +78,8 @@ class ExifInfo { other.timeZone == timeZone && other.dateTimeOriginal == dateTimeOriginal && other.rating == rating && + other.width == width && + other.height == height && other.latitude == latitude && other.longitude == longitude && other.city == city && @@ -98,6 +104,8 @@ class ExifInfo { timeZone.hashCode ^ dateTimeOriginal.hashCode ^ rating.hashCode ^ + width.hashCode ^ + height.hashCode ^ latitude.hashCode ^ longitude.hashCode ^ city.hashCode ^ @@ -123,6 +131,8 @@ isFlipped: $isFlipped, timeZone: ${timeZone ?? 'NA'}, dateTimeOriginal: ${dateTimeOriginal ?? 'NA'}, rating: ${rating ?? 'NA'}, +width: ${width ?? 'NA'}, +height: ${height ?? 'NA'}, latitude: ${latitude ?? 'NA'}, longitude: ${longitude ?? 'NA'}, city: ${city ?? 'NA'}, @@ -146,6 +156,8 @@ exposureSeconds: ${exposureSeconds ?? 'NA'}, String? timeZone, DateTime? dateTimeOriginal, int? rating, + int? width, + int? height, double? latitude, double? longitude, String? city, @@ -168,6 +180,8 @@ exposureSeconds: ${exposureSeconds ?? 'NA'}, timeZone: timeZone ?? this.timeZone, dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, rating: rating ?? this.rating, + width: width ?? this.width, + height: height ?? this.height, isFlipped: isFlipped ?? this.isFlipped, latitude: latitude ?? this.latitude, longitude: longitude ?? this.longitude, diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index 198733b3c8..7fa8c13fd8 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -1,12 +1,9 @@ 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'; -typedef _AssetVideoDimension = ({double? width, double? height, bool isFlipped}); - class AssetService { final RemoteAssetRepository _remoteAssetRepository; final DriftLocalAssetRepository _localAssetRepository; @@ -58,49 +55,6 @@ class AssetService { return _remoteAssetRepository.getExif(id); } - Future getAspectRatio(BaseAsset asset) async { - final dimension = asset is LocalAsset - ? await _getLocalAssetDimensions(asset) - : await _getRemoteAssetDimensions(asset as RemoteAsset); - - if (dimension.width == null || dimension.height == null || dimension.height == 0) { - return 1.0; - } - - return dimension.isFlipped ? dimension.height! / dimension.width! : dimension.width! / dimension.height!; - } - - Future<_AssetVideoDimension> _getLocalAssetDimensions(LocalAsset asset) async { - double? width = asset.width?.toDouble(); - double? height = asset.height?.toDouble(); - int orientation = asset.orientation; - - if (width == null || height == null) { - final fetched = await _localAssetRepository.get(asset.id); - width = fetched?.width?.toDouble(); - height = fetched?.height?.toDouble(); - orientation = fetched?.orientation ?? 0; - } - - // On Android, local assets need orientation correction for 90°/270° rotations - // On iOS, the Photos framework pre-corrects dimensions - final isFlipped = CurrentPlatform.isAndroid && (orientation == 90 || orientation == 270); - return (width: width, height: height, isFlipped: isFlipped); - } - - Future<_AssetVideoDimension> _getRemoteAssetDimensions(RemoteAsset asset) async { - double? width = asset.width?.toDouble(); - double? height = asset.height?.toDouble(); - - if (width == null || height == null) { - final fetched = await _remoteAssetRepository.get(asset.id); - width = fetched?.width?.toDouble(); - height = fetched?.height?.toDouble(); - } - - return (width: width, height: height, isFlipped: false); - } - Future> getPlaces(String userId) { return _remoteAssetRepository.getPlaces(userId); } diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 93a2a14127..d4da3e31a4 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -16,19 +16,16 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart' show nativeSyncApiProvider; 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/foreground_upload.service.dart'; +import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/wm_executor.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; class BackgroundWorkerFgService { @@ -58,7 +55,6 @@ class BackgroundWorkerFgService { class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { ProviderContainer? _ref; - final Isar _isar; final Drift _drift; final DriftLogger _driftLogger; final BackgroundWorkerBgHostApi _backgroundHostApi; @@ -67,18 +63,11 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { bool _isCleanedUp = false; - BackgroundWorkerBgService({required Isar isar, required Drift drift, required DriftLogger driftLogger}) - : _isar = isar, - _drift = drift, + BackgroundWorkerBgService({required Drift drift, required DriftLogger driftLogger}) + : _drift = drift, _driftLogger = driftLogger, _backgroundHostApi = BackgroundWorkerBgHostApi() { - _ref = ProviderContainer( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - ); + _ref = ProviderContainer(overrides: [driftProvider.overrideWith(driftOverride(drift))]); BackgroundWorkerFlutterApi.setUp(this); } @@ -102,7 +91,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { ), FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false), FileDownloader().trackTasks(), - _ref?.read(fileMediaRepositoryProvider).enableBackgroundAccess(), ].nonNulls, ); @@ -209,9 +197,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { backgroundSyncManager?.cancel(), ]; - if (_isar.isOpen) { - cleanupFutures.add(_isar.close()); - } await Future.wait(cleanupFutures.nonNulls); _logger.info("Background worker resources cleaned up"); } catch (error, stack) { @@ -301,7 +286,6 @@ Future backgroundSyncNativeEntrypoint() async { WidgetsFlutterBinding.ensureInitialized(); DartPluginRegistrant.ensureInitialized(); - final (isar, drift, logDB) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false, listenStoreUpdates: false); - await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init(); + final (drift, logDB) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false); + await BackgroundWorkerBgService(drift: drift, driftLogger: logDB).init(); } diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart index 64010b9220..b58ee89535 100644 --- a/mobile/lib/domain/services/log.service.dart +++ b/mobile/lib/domain/services/log.service.dart @@ -15,7 +15,7 @@ import 'package:logging/logging.dart'; /// via [IStoreRepository] class LogService { final LogRepository _logRepository; - final IStoreRepository _storeRepository; + final DriftStoreRepository _storeRepository; final List _msgBuffer = []; @@ -38,7 +38,7 @@ class LogService { static Future init({ required LogRepository logRepository, - required IStoreRepository storeRepository, + required DriftStoreRepository storeRepository, bool shouldBuffer = true, }) async { _instance ??= await create( @@ -51,7 +51,7 @@ class LogService { static Future create({ required LogRepository logRepository, - required IStoreRepository storeRepository, + required DriftStoreRepository storeRepository, bool shouldBuffer = true, }) async { final instance = LogService._(logRepository, storeRepository, shouldBuffer); diff --git a/mobile/lib/domain/services/search.service.dart b/mobile/lib/domain/services/search.service.dart index 004ad06b1b..8b93e9c8cc 100644 --- a/mobile/lib/domain/services/search.service.dart +++ b/mobile/lib/domain/services/search.service.dart @@ -1,10 +1,9 @@ -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/search_result.model.dart'; +import 'package:immich_mobile/extensions/asset_extensions.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart' as api show AssetVisibility; import 'package:openapi/api.dart' hide AssetVisibility; class SearchService { @@ -52,43 +51,3 @@ class SearchService { return null; } } - -extension on AssetResponseDto { - RemoteAsset toDto() { - return RemoteAsset( - id: id, - name: originalFileName, - checksum: checksum, - createdAt: fileCreatedAt, - updatedAt: updatedAt, - ownerId: ownerId, - visibility: switch (visibility) { - api.AssetVisibility.timeline => AssetVisibility.timeline, - api.AssetVisibility.hidden => AssetVisibility.hidden, - api.AssetVisibility.archive => AssetVisibility.archive, - api.AssetVisibility.locked => AssetVisibility.locked, - _ => AssetVisibility.timeline, - }, - durationInSeconds: duration.toDuration()?.inSeconds ?? 0, - height: height?.toInt(), - width: width?.toInt(), - isFavorite: isFavorite, - livePhotoVideoId: livePhotoVideoId, - thumbHash: thumbhash, - localId: null, - type: type.toAssetType(), - stackId: stack?.id, - isEdited: isEdited, - ); - } -} - -extension on AssetTypeEnum { - AssetType toAssetType() => switch (this) { - AssetTypeEnum.IMAGE => AssetType.image, - AssetTypeEnum.VIDEO => AssetType.video, - AssetTypeEnum.AUDIO => AssetType.audio, - AssetTypeEnum.OTHER => AssetType.other, - _ => throw Exception('Unknown AssetType value: $this'), - }; -} diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart index 0098c3d262..b325ffd631 100644 --- a/mobile/lib/domain/services/store.service.dart +++ b/mobile/lib/domain/services/store.service.dart @@ -6,13 +6,13 @@ import 'package:immich_mobile/infrastructure/repositories/store.repository.dart' /// Provides access to a persistent key-value store with an in-memory cache. /// Listens for repository changes to keep the cache updated. class StoreService { - final IStoreRepository _storeRepository; + final DriftStoreRepository _storeRepository; /// In-memory cache. Keys are [StoreKey.id] final Map _cache = {}; StreamSubscription>? _storeUpdateSubscription; - StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository; + StoreService._({required DriftStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository; // TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider static StoreService? _instance; @@ -24,12 +24,12 @@ class StoreService { } // TODO: Replace the implementation with the one from create after removing the typedef - static Future init({required IStoreRepository storeRepository, bool listenUpdates = true}) async { + static Future init({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async { _instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates); return _instance!; } - static Future create({required IStoreRepository storeRepository, bool listenUpdates = true}) async { + static Future create({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async { final instance = StoreService._(isarStoreRepository: storeRepository); await instance.populateCache(); if (listenUpdates) { @@ -91,8 +91,6 @@ class StoreService { await _storeRepository.deleteAll(); _cache.clear(); } - - bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? true; } class StoreKeyNotFoundException implements Exception { diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index b33940eacd..a055f8bcae 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -34,6 +34,7 @@ enum TimelineOrigin { search, deepLink, albumActivities, + folder, } class TimelineFactory { diff --git a/mobile/lib/domain/services/user.service.dart b/mobile/lib/domain/services/user.service.dart index d347d8aa4f..1f9c015ad7 100644 --- a/mobile/lib/domain/services/user.service.dart +++ b/mobile/lib/domain/services/user.service.dart @@ -4,23 +4,17 @@ import 'dart:typed_data'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:logging/logging.dart'; class UserService { final Logger _log = Logger("UserService"); - final IsarUserRepository _isarUserRepository; final UserApiRepository _userApiRepository; final StoreService _storeService; - UserService({ - required IsarUserRepository isarUserRepository, - required UserApiRepository userApiRepository, - required StoreService storeService, - }) : _isarUserRepository = isarUserRepository, - _userApiRepository = userApiRepository, - _storeService = storeService; + UserService({required UserApiRepository userApiRepository, required StoreService storeService}) + : _userApiRepository = userApiRepository, + _storeService = storeService; UserDto getMyUser() { return _storeService.get(StoreKey.currentUser); @@ -38,7 +32,6 @@ class UserService { final user = await _userApiRepository.getMyUser(); if (user == null) return null; await _storeService.put(StoreKey.currentUser, user); - await _isarUserRepository.update(user); return user; } @@ -47,19 +40,10 @@ class UserService { final path = await _userApiRepository.createProfileImage(name: name, data: image); final updatedUser = getMyUser(); await _storeService.put(StoreKey.currentUser, updatedUser); - await _isarUserRepository.update(updatedUser); return path; } catch (e) { _log.warning("Failed to upload profile image", e); return null; } } - - Future> getAll() async { - return await _isarUserRepository.getAll(); - } - - Future deleteAll() { - return _isarUserRepository.deleteAll(); - } } diff --git a/mobile/lib/domain/utils/migrate_cloud_ids.dart b/mobile/lib/domain/utils/migrate_cloud_ids.dart index 33a8eca94d..32188b4838 100644 --- a/mobile/lib/domain/utils/migrate_cloud_ids.dart +++ b/mobile/lib/domain/utils/migrate_cloud_ids.dart @@ -80,12 +80,14 @@ Future _processCloudIdMappingsInBatches( AssetMetadataBulkUpsertItemDto( assetId: mapping.remoteAssetId, key: kMobileMetadataKey, - value: RemoteAssetMobileAppMetadata( - cloudId: mapping.localAsset.cloudId, - createdAt: mapping.localAsset.createdAt.toIso8601String(), - adjustmentTime: mapping.localAsset.adjustmentTime?.toIso8601String(), - latitude: mapping.localAsset.latitude?.toString(), - longitude: mapping.localAsset.longitude?.toString(), + value: Map.from( + RemoteAssetMobileAppMetadata( + cloudId: mapping.localAsset.cloudId, + createdAt: mapping.localAsset.createdAt.toIso8601String(), + adjustmentTime: mapping.localAsset.adjustmentTime?.toIso8601String(), + latitude: mapping.localAsset.latitude?.toString(), + longitude: mapping.localAsset.longitude?.toString(), + ).toJson(), ), ), ); diff --git a/mobile/lib/entities/README.md b/mobile/lib/entities/README.md deleted file mode 100644 index c2ad4876e3..0000000000 --- a/mobile/lib/entities/README.md +++ /dev/null @@ -1 +0,0 @@ -This directory contains entity that is stored in the local storage. \ No newline at end of file diff --git a/mobile/lib/entities/album.entity.dart b/mobile/lib/entities/album.entity.dart deleted file mode 100644 index 2ca0d50dcc..0000000000 --- a/mobile/lib/entities/album.entity.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/utils/datetime_comparison.dart'; -import 'package:isar/isar.dart'; -// ignore: implementation_imports -import 'package:isar/src/common/isar_links_common.dart'; -import 'package:openapi/api.dart'; - -part 'album.entity.g.dart'; - -@Collection(inheritance: false) -class Album { - @protected - Album({ - this.remoteId, - this.localId, - required this.name, - required this.createdAt, - required this.modifiedAt, - this.description, - this.startDate, - this.endDate, - this.lastModifiedAssetTimestamp, - required this.shared, - required this.activityEnabled, - this.sortOrder = SortOrder.desc, - }); - - // fields stored in DB - Id id = Isar.autoIncrement; - @Index(unique: false, replace: false, type: IndexType.hash) - String? remoteId; - @Index(unique: false, replace: false, type: IndexType.hash) - String? localId; - String name; - String? description; - DateTime createdAt; - DateTime modifiedAt; - DateTime? startDate; - DateTime? endDate; - DateTime? lastModifiedAssetTimestamp; - bool shared; - bool activityEnabled; - @enumerated - SortOrder sortOrder; - final IsarLink owner = IsarLink(); - final IsarLink thumbnail = IsarLink(); - final IsarLinks sharedUsers = IsarLinks(); - final IsarLinks assets = IsarLinks(); - - // transient fields - @ignore - bool isAll = false; - - @ignore - String? remoteThumbnailAssetId; - - @ignore - int remoteAssetCount = 0; - - // getters - @ignore - bool get isRemote => remoteId != null; - - @ignore - bool get isLocal => localId != null; - - @ignore - int get assetCount => assets.length; - - @ignore - String? get ownerId => owner.value?.id; - - @ignore - String? get ownerName { - // Guard null owner - if (owner.value == null) { - return null; - } - - final name = []; - if (owner.value?.name != null) { - name.add(owner.value!.name); - } - - return name.join(' '); - } - - @ignore - String get eTagKeyAssetCount => "device-album-$localId-asset-count"; - - // the following getter are needed because Isar links do not make data - // accessible in an object freshly created (not loaded from DB) - - @ignore - Iterable get remoteUsers => - sharedUsers.isEmpty ? (sharedUsers as IsarLinksCommon).addedObjects : sharedUsers; - - @ignore - Iterable get remoteAssets => assets.isEmpty ? (assets as IsarLinksCommon).addedObjects : assets; - - @override - bool operator ==(other) { - if (other is! Album) return false; - return id == other.id && - remoteId == other.remoteId && - localId == other.localId && - name == other.name && - description == other.description && - createdAt.isAtSameMomentAs(other.createdAt) && - modifiedAt.isAtSameMomentAs(other.modifiedAt) && - isAtSameMomentAs(startDate, other.startDate) && - isAtSameMomentAs(endDate, other.endDate) && - isAtSameMomentAs(lastModifiedAssetTimestamp, other.lastModifiedAssetTimestamp) && - shared == other.shared && - activityEnabled == other.activityEnabled && - owner.value == other.owner.value && - thumbnail.value == other.thumbnail.value && - sharedUsers.length == other.sharedUsers.length && - assets.length == other.assets.length; - } - - @override - @ignore - int get hashCode => - id.hashCode ^ - remoteId.hashCode ^ - localId.hashCode ^ - name.hashCode ^ - createdAt.hashCode ^ - modifiedAt.hashCode ^ - startDate.hashCode ^ - endDate.hashCode ^ - description.hashCode ^ - lastModifiedAssetTimestamp.hashCode ^ - shared.hashCode ^ - activityEnabled.hashCode ^ - owner.value.hashCode ^ - thumbnail.value.hashCode ^ - sharedUsers.length.hashCode ^ - assets.length.hashCode; - - static Future remote(AlbumResponseDto dto) async { - final Isar db = Isar.getInstance()!; - final Album a = Album( - remoteId: dto.id, - name: dto.albumName, - createdAt: dto.createdAt, - modifiedAt: dto.updatedAt, - description: dto.description, - lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp, - shared: dto.shared, - startDate: dto.startDate, - endDate: dto.endDate, - activityEnabled: dto.isActivityEnabled, - ); - a.remoteAssetCount = dto.assetCount; - a.owner.value = await db.users.getById(dto.ownerId); - if (dto.order != null) { - a.sortOrder = dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc; - } - - if (dto.albumThumbnailAssetId != null) { - a.thumbnail.value = await db.assets.where().remoteIdEqualTo(dto.albumThumbnailAssetId).findFirst(); - } - if (dto.albumUsers.isNotEmpty) { - final users = await db.users.getAllById(dto.albumUsers.map((e) => e.user.id).toList(growable: false)); - a.sharedUsers.addAll(users.cast()); - } - if (dto.assets.isNotEmpty) { - final assets = await db.assets.getAllByRemoteId(dto.assets.map((e) => e.id)); - a.assets.addAll(assets); - } - return a; - } - - @override - String toString() => 'remoteId: $remoteId name: $name description: $description'; -} - -extension AssetsHelper on IsarCollection { - Future store(Album a) async { - await put(a); - await a.owner.save(); - await a.thumbnail.save(); - await a.sharedUsers.save(); - await a.assets.save(); - return a; - } -} diff --git a/mobile/lib/entities/album.entity.g.dart b/mobile/lib/entities/album.entity.g.dart deleted file mode 100644 index ecbbab48c2..0000000000 --- a/mobile/lib/entities/album.entity.g.dart +++ /dev/null @@ -1,2240 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'album.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetAlbumCollection on Isar { - IsarCollection get albums => this.collection(); -} - -const AlbumSchema = CollectionSchema( - name: r'Album', - id: -1355968412107120937, - properties: { - r'activityEnabled': PropertySchema( - id: 0, - name: r'activityEnabled', - type: IsarType.bool, - ), - r'createdAt': PropertySchema( - id: 1, - name: r'createdAt', - type: IsarType.dateTime, - ), - r'description': PropertySchema( - id: 2, - name: r'description', - type: IsarType.string, - ), - r'endDate': PropertySchema( - id: 3, - name: r'endDate', - type: IsarType.dateTime, - ), - r'lastModifiedAssetTimestamp': PropertySchema( - id: 4, - name: r'lastModifiedAssetTimestamp', - type: IsarType.dateTime, - ), - r'localId': PropertySchema(id: 5, name: r'localId', type: IsarType.string), - r'modifiedAt': PropertySchema( - id: 6, - name: r'modifiedAt', - type: IsarType.dateTime, - ), - r'name': PropertySchema(id: 7, name: r'name', type: IsarType.string), - r'remoteId': PropertySchema( - id: 8, - name: r'remoteId', - type: IsarType.string, - ), - r'shared': PropertySchema(id: 9, name: r'shared', type: IsarType.bool), - r'sortOrder': PropertySchema( - id: 10, - name: r'sortOrder', - type: IsarType.byte, - enumMap: _AlbumsortOrderEnumValueMap, - ), - r'startDate': PropertySchema( - id: 11, - name: r'startDate', - type: IsarType.dateTime, - ), - }, - - estimateSize: _albumEstimateSize, - serialize: _albumSerialize, - deserialize: _albumDeserialize, - deserializeProp: _albumDeserializeProp, - idName: r'id', - indexes: { - r'remoteId': IndexSchema( - id: 6301175856541681032, - name: r'remoteId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'remoteId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'localId': IndexSchema( - id: 1199848425898359622, - name: r'localId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'localId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: { - r'owner': LinkSchema( - id: 8272576585804958029, - name: r'owner', - target: r'User', - single: true, - ), - r'thumbnail': LinkSchema( - id: 4055421409629988258, - name: r'thumbnail', - target: r'Asset', - single: true, - ), - r'sharedUsers': LinkSchema( - id: 8972835302564625434, - name: r'sharedUsers', - target: r'User', - single: false, - ), - r'assets': LinkSchema( - id: 1059358332698388152, - name: r'assets', - target: r'Asset', - single: false, - ), - }, - embeddedSchemas: {}, - - getId: _albumGetId, - getLinks: _albumGetLinks, - attach: _albumAttach, - version: '3.3.0-dev.3', -); - -int _albumEstimateSize( - Album object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.description; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.localId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - bytesCount += 3 + object.name.length * 3; - { - final value = object.remoteId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _albumSerialize( - Album object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeBool(offsets[0], object.activityEnabled); - writer.writeDateTime(offsets[1], object.createdAt); - writer.writeString(offsets[2], object.description); - writer.writeDateTime(offsets[3], object.endDate); - writer.writeDateTime(offsets[4], object.lastModifiedAssetTimestamp); - writer.writeString(offsets[5], object.localId); - writer.writeDateTime(offsets[6], object.modifiedAt); - writer.writeString(offsets[7], object.name); - writer.writeString(offsets[8], object.remoteId); - writer.writeBool(offsets[9], object.shared); - writer.writeByte(offsets[10], object.sortOrder.index); - writer.writeDateTime(offsets[11], object.startDate); -} - -Album _albumDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = Album( - activityEnabled: reader.readBool(offsets[0]), - createdAt: reader.readDateTime(offsets[1]), - description: reader.readStringOrNull(offsets[2]), - endDate: reader.readDateTimeOrNull(offsets[3]), - lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[4]), - localId: reader.readStringOrNull(offsets[5]), - modifiedAt: reader.readDateTime(offsets[6]), - name: reader.readString(offsets[7]), - remoteId: reader.readStringOrNull(offsets[8]), - shared: reader.readBool(offsets[9]), - sortOrder: - _AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[10])] ?? - SortOrder.desc, - startDate: reader.readDateTimeOrNull(offsets[11]), - ); - object.id = id; - return object; -} - -P _albumDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readBool(offset)) as P; - case 1: - return (reader.readDateTime(offset)) as P; - case 2: - return (reader.readStringOrNull(offset)) as P; - case 3: - return (reader.readDateTimeOrNull(offset)) as P; - case 4: - return (reader.readDateTimeOrNull(offset)) as P; - case 5: - return (reader.readStringOrNull(offset)) as P; - case 6: - return (reader.readDateTime(offset)) as P; - case 7: - return (reader.readString(offset)) as P; - case 8: - return (reader.readStringOrNull(offset)) as P; - case 9: - return (reader.readBool(offset)) as P; - case 10: - return (_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offset)] ?? - SortOrder.desc) - as P; - case 11: - return (reader.readDateTimeOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _AlbumsortOrderEnumValueMap = {'asc': 0, 'desc': 1}; -const _AlbumsortOrderValueEnumMap = {0: SortOrder.asc, 1: SortOrder.desc}; - -Id _albumGetId(Album object) { - return object.id; -} - -List> _albumGetLinks(Album object) { - return [object.owner, object.thumbnail, object.sharedUsers, object.assets]; -} - -void _albumAttach(IsarCollection col, Id id, Album object) { - object.id = id; - object.owner.attach(col, col.isar.collection(), r'owner', id); - object.thumbnail.attach(col, col.isar.collection(), r'thumbnail', id); - object.sharedUsers.attach( - col, - col.isar.collection(), - r'sharedUsers', - id, - ); - object.assets.attach(col, col.isar.collection(), r'assets', id); -} - -extension AlbumQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension AlbumQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [null]), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [remoteId]), - ); - }); - } - - QueryBuilder remoteIdNotEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [null]), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [localId]), - ); - }); - } - - QueryBuilder localIdNotEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ); - } - }); - } -} - -extension AlbumQueryFilter on QueryBuilder { - QueryBuilder activityEnabledEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'activityEnabled', value: value), - ); - }); - } - - QueryBuilder createdAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'createdAt', value: value), - ); - }); - } - - QueryBuilder createdAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'createdAt', - value: value, - ), - ); - }); - } - - QueryBuilder createdAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'createdAt', - value: value, - ), - ); - }); - } - - QueryBuilder createdAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'createdAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder descriptionIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'description'), - ); - }); - } - - QueryBuilder descriptionIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'description'), - ); - }); - } - - QueryBuilder descriptionEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'description', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'description', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'description', value: ''), - ); - }); - } - - QueryBuilder descriptionIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'description', value: ''), - ); - }); - } - - QueryBuilder endDateIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'endDate'), - ); - }); - } - - QueryBuilder endDateIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'endDate'), - ); - }); - } - - QueryBuilder endDateEqualTo( - DateTime? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'endDate', value: value), - ); - }); - } - - QueryBuilder endDateGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'endDate', - value: value, - ), - ); - }); - } - - QueryBuilder endDateLessThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'endDate', - value: value, - ), - ); - }); - } - - QueryBuilder endDateBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'endDate', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'lastModifiedAssetTimestamp'), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull( - property: r'lastModifiedAssetTimestamp', - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'lastModifiedAssetTimestamp', - value: value, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lastModifiedAssetTimestamp', - value: value, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampLessThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lastModifiedAssetTimestamp', - value: value, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lastModifiedAssetTimestamp', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'localId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'localId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder localIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder modifiedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'modifiedAt', value: value), - ); - }); - } - - QueryBuilder modifiedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'modifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder modifiedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'modifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder modifiedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'modifiedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder nameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'name', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'name', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'name', value: ''), - ); - }); - } - - QueryBuilder nameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'name', value: ''), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'remoteId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'remoteId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder remoteIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder sharedEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'shared', value: value), - ); - }); - } - - QueryBuilder sortOrderEqualTo( - SortOrder value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'sortOrder', value: value), - ); - }); - } - - QueryBuilder sortOrderGreaterThan( - SortOrder value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'sortOrder', - value: value, - ), - ); - }); - } - - QueryBuilder sortOrderLessThan( - SortOrder value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'sortOrder', - value: value, - ), - ); - }); - } - - QueryBuilder sortOrderBetween( - SortOrder lower, - SortOrder upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'sortOrder', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder startDateIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'startDate'), - ); - }); - } - - QueryBuilder startDateIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'startDate'), - ); - }); - } - - QueryBuilder startDateEqualTo( - DateTime? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'startDate', value: value), - ); - }); - } - - QueryBuilder startDateGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'startDate', - value: value, - ), - ); - }); - } - - QueryBuilder startDateLessThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'startDate', - value: value, - ), - ); - }); - } - - QueryBuilder startDateBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'startDate', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension AlbumQueryObject on QueryBuilder {} - -extension AlbumQueryLinks on QueryBuilder { - QueryBuilder owner(FilterQuery q) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'owner'); - }); - } - - QueryBuilder ownerIsNull() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'owner', 0, true, 0, true); - }); - } - - QueryBuilder thumbnail( - FilterQuery q, - ) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'thumbnail'); - }); - } - - QueryBuilder thumbnailIsNull() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'thumbnail', 0, true, 0, true); - }); - } - - QueryBuilder sharedUsers( - FilterQuery q, - ) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'sharedUsers'); - }); - } - - QueryBuilder sharedUsersLengthEqualTo( - int length, - ) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', length, true, length, true); - }); - } - - QueryBuilder sharedUsersIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', 0, true, 0, true); - }); - } - - QueryBuilder sharedUsersIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', 0, false, 999999, true); - }); - } - - QueryBuilder sharedUsersLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', 0, true, length, include); - }); - } - - QueryBuilder - sharedUsersLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', length, include, 999999, true); - }); - } - - QueryBuilder sharedUsersLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength( - r'sharedUsers', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder assets( - FilterQuery q, - ) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'assets'); - }); - } - - QueryBuilder assetsLengthEqualTo( - int length, - ) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', length, true, length, true); - }); - } - - QueryBuilder assetsIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', 0, true, 0, true); - }); - } - - QueryBuilder assetsIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', 0, false, 999999, true); - }); - } - - QueryBuilder assetsLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', 0, true, length, include); - }); - } - - QueryBuilder assetsLengthGreaterThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', length, include, 999999, true); - }); - } - - QueryBuilder assetsLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength( - r'assets', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } -} - -extension AlbumQuerySortBy on QueryBuilder { - QueryBuilder sortByActivityEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.asc); - }); - } - - QueryBuilder sortByActivityEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.desc); - }); - } - - QueryBuilder sortByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.asc); - }); - } - - QueryBuilder sortByCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.desc); - }); - } - - QueryBuilder sortByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder sortByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder sortByEndDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.asc); - }); - } - - QueryBuilder sortByEndDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.desc); - }); - } - - QueryBuilder sortByLastModifiedAssetTimestamp() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc); - }); - } - - QueryBuilder - sortByLastModifiedAssetTimestampDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc); - }); - } - - QueryBuilder sortByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder sortByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder sortByModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.asc); - }); - } - - QueryBuilder sortByModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.desc); - }); - } - - QueryBuilder sortByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder sortByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder sortByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder sortByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder sortByShared() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.asc); - }); - } - - QueryBuilder sortBySharedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.desc); - }); - } - - QueryBuilder sortBySortOrder() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.asc); - }); - } - - QueryBuilder sortBySortOrderDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.desc); - }); - } - - QueryBuilder sortByStartDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.asc); - }); - } - - QueryBuilder sortByStartDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.desc); - }); - } -} - -extension AlbumQuerySortThenBy on QueryBuilder { - QueryBuilder thenByActivityEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.asc); - }); - } - - QueryBuilder thenByActivityEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.desc); - }); - } - - QueryBuilder thenByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.asc); - }); - } - - QueryBuilder thenByCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.desc); - }); - } - - QueryBuilder thenByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder thenByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder thenByEndDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.asc); - }); - } - - QueryBuilder thenByEndDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByLastModifiedAssetTimestamp() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc); - }); - } - - QueryBuilder - thenByLastModifiedAssetTimestampDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc); - }); - } - - QueryBuilder thenByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder thenByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder thenByModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.asc); - }); - } - - QueryBuilder thenByModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.desc); - }); - } - - QueryBuilder thenByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder thenByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder thenByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder thenByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder thenByShared() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.asc); - }); - } - - QueryBuilder thenBySharedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.desc); - }); - } - - QueryBuilder thenBySortOrder() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.asc); - }); - } - - QueryBuilder thenBySortOrderDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.desc); - }); - } - - QueryBuilder thenByStartDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.asc); - }); - } - - QueryBuilder thenByStartDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.desc); - }); - } -} - -extension AlbumQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByActivityEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'activityEnabled'); - }); - } - - QueryBuilder distinctByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'createdAt'); - }); - } - - QueryBuilder distinctByDescription({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'description', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByEndDate() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'endDate'); - }); - } - - QueryBuilder distinctByLastModifiedAssetTimestamp() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lastModifiedAssetTimestamp'); - }); - } - - QueryBuilder distinctByLocalId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'localId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'modifiedAt'); - }); - } - - QueryBuilder distinctByName({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'name', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByRemoteId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'remoteId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByShared() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'shared'); - }); - } - - QueryBuilder distinctBySortOrder() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'sortOrder'); - }); - } - - QueryBuilder distinctByStartDate() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'startDate'); - }); - } -} - -extension AlbumQueryProperty on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder activityEnabledProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'activityEnabled'); - }); - } - - QueryBuilder createdAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'createdAt'); - }); - } - - QueryBuilder descriptionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'description'); - }); - } - - QueryBuilder endDateProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'endDate'); - }); - } - - QueryBuilder - lastModifiedAssetTimestampProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lastModifiedAssetTimestamp'); - }); - } - - QueryBuilder localIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'localId'); - }); - } - - QueryBuilder modifiedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'modifiedAt'); - }); - } - - QueryBuilder nameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'name'); - }); - } - - QueryBuilder remoteIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'remoteId'); - }); - } - - QueryBuilder sharedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'shared'); - }); - } - - QueryBuilder sortOrderProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'sortOrder'); - }); - } - - QueryBuilder startDateProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'startDate'); - }); - } -} diff --git a/mobile/lib/entities/android_device_asset.entity.dart b/mobile/lib/entities/android_device_asset.entity.dart deleted file mode 100644 index 792de346b9..0000000000 --- a/mobile/lib/entities/android_device_asset.entity.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:immich_mobile/entities/device_asset.entity.dart'; -import 'package:isar/isar.dart'; - -part 'android_device_asset.entity.g.dart'; - -@Collection() -class AndroidDeviceAsset extends DeviceAsset { - AndroidDeviceAsset({required this.id, required super.hash}); - Id id; -} diff --git a/mobile/lib/entities/android_device_asset.entity.g.dart b/mobile/lib/entities/android_device_asset.entity.g.dart deleted file mode 100644 index f8b1e32c72..0000000000 --- a/mobile/lib/entities/android_device_asset.entity.g.dart +++ /dev/null @@ -1,463 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'android_device_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetAndroidDeviceAssetCollection on Isar { - IsarCollection get androidDeviceAssets => - this.collection(); -} - -const AndroidDeviceAssetSchema = CollectionSchema( - name: r'AndroidDeviceAsset', - id: -6758387181232899335, - properties: { - r'hash': PropertySchema(id: 0, name: r'hash', type: IsarType.byteList), - }, - - estimateSize: _androidDeviceAssetEstimateSize, - serialize: _androidDeviceAssetSerialize, - deserialize: _androidDeviceAssetDeserialize, - deserializeProp: _androidDeviceAssetDeserializeProp, - idName: r'id', - indexes: { - r'hash': IndexSchema( - id: -7973251393006690288, - name: r'hash', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'hash', - type: IndexType.hash, - caseSensitive: false, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _androidDeviceAssetGetId, - getLinks: _androidDeviceAssetGetLinks, - attach: _androidDeviceAssetAttach, - version: '3.3.0-dev.3', -); - -int _androidDeviceAssetEstimateSize( - AndroidDeviceAsset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.hash.length; - return bytesCount; -} - -void _androidDeviceAssetSerialize( - AndroidDeviceAsset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByteList(offsets[0], object.hash); -} - -AndroidDeviceAsset _androidDeviceAssetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = AndroidDeviceAsset( - hash: reader.readByteList(offsets[0]) ?? [], - id: id, - ); - return object; -} - -P _androidDeviceAssetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readByteList(offset) ?? []) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _androidDeviceAssetGetId(AndroidDeviceAsset object) { - return object.id; -} - -List> _androidDeviceAssetGetLinks( - AndroidDeviceAsset object, -) { - return []; -} - -void _androidDeviceAssetAttach( - IsarCollection col, - Id id, - AndroidDeviceAsset object, -) { - object.id = id; -} - -extension AndroidDeviceAssetQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension AndroidDeviceAssetQueryWhere - on QueryBuilder { - QueryBuilder - idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder - idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder - idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder - idLessThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder - idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'hash', value: [hash]), - ); - }); - } - - QueryBuilder - hashNotEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ); - } - }); - } -} - -extension AndroidDeviceAssetQueryFilter - on QueryBuilder { - QueryBuilder - hashElementEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'hash', value: value), - ); - }); - } - - QueryBuilder - hashElementGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'hash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, true, length, true); - }); - } - - QueryBuilder - hashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, 0, true); - }); - } - - QueryBuilder - hashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, false, 999999, true); - }); - } - - QueryBuilder - hashLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, length, include); - }); - } - - QueryBuilder - hashLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, include, 999999, true); - }); - } - - QueryBuilder - hashLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'hash', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder - idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder - idGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension AndroidDeviceAssetQueryObject - on QueryBuilder {} - -extension AndroidDeviceAssetQueryLinks - on QueryBuilder {} - -extension AndroidDeviceAssetQuerySortBy - on QueryBuilder {} - -extension AndroidDeviceAssetQuerySortThenBy - on QueryBuilder { - QueryBuilder - thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder - thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension AndroidDeviceAssetQueryWhereDistinct - on QueryBuilder { - QueryBuilder - distinctByHash() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hash'); - }); - } -} - -extension AndroidDeviceAssetQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder, QQueryOperations> hashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hash'); - }); - } -} diff --git a/mobile/lib/entities/asset.entity.dart b/mobile/lib/entities/asset.entity.dart deleted file mode 100644 index 0d549457a1..0000000000 --- a/mobile/lib/entities/asset.entity.dart +++ /dev/null @@ -1,575 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; -import 'package:openapi/api.dart'; -import 'package:path/path.dart' as p; -import 'package:photo_manager/photo_manager.dart' show AssetEntity; - -part 'asset.entity.g.dart'; - -/// Asset (online or local) -@Collection(inheritance: false) -class Asset { - Asset.remote(AssetResponseDto remote) - : remoteId = remote.id, - checksum = remote.checksum, - fileCreatedAt = remote.fileCreatedAt, - fileModifiedAt = remote.fileModifiedAt, - updatedAt = remote.updatedAt, - durationInSeconds = remote.duration.toDuration()?.inSeconds ?? 0, - type = remote.type.toAssetType(), - fileName = remote.originalFileName, - height = remote.exifInfo?.exifImageHeight?.toInt(), - width = remote.exifInfo?.exifImageWidth?.toInt(), - livePhotoVideoId = remote.livePhotoVideoId, - ownerId = fastHash(remote.ownerId), - exifInfo = remote.exifInfo == null ? null : ExifDtoConverter.fromDto(remote.exifInfo!), - isFavorite = remote.isFavorite, - isArchived = remote.isArchived, - isTrashed = remote.isTrashed, - isOffline = remote.isOffline, - // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app - // stack handling to properly handle it - stackPrimaryAssetId = remote.stack?.primaryAssetId == remote.id ? null : remote.stack?.primaryAssetId, - stackCount = remote.stack?.assetCount ?? 0, - stackId = remote.stack?.id, - thumbhash = remote.thumbhash, - visibility = getVisibility(remote.visibility); - - Asset({ - this.id = Isar.autoIncrement, - required this.checksum, - this.remoteId, - required this.localId, - required this.ownerId, - required this.fileCreatedAt, - required this.fileModifiedAt, - required this.updatedAt, - required this.durationInSeconds, - required this.type, - this.width, - this.height, - required this.fileName, - this.livePhotoVideoId, - this.exifInfo, - this.isFavorite = false, - this.isArchived = false, - this.isTrashed = false, - this.stackId, - this.stackPrimaryAssetId, - this.stackCount = 0, - this.isOffline = false, - this.thumbhash, - this.visibility = AssetVisibilityEnum.timeline, - }); - - @ignore - AssetEntity? _local; - - @ignore - AssetEntity? get local { - if (isLocal && _local == null) { - _local = AssetEntity( - id: localId!, - typeInt: isImage ? 1 : 2, - width: width ?? 0, - height: height ?? 0, - duration: durationInSeconds, - createDateSecond: fileCreatedAt.millisecondsSinceEpoch ~/ 1000, - modifiedDateSecond: fileModifiedAt.millisecondsSinceEpoch ~/ 1000, - title: fileName, - ); - } - return _local; - } - - set local(AssetEntity? assetEntity) => _local = assetEntity; - - @ignore - bool _didUpdateLocal = false; - - @ignore - Future get localAsync async { - final local = this.local; - if (local == null) { - throw Exception('Asset $fileName has no local data'); - } - - final updatedLocal = _didUpdateLocal ? local : await local.obtainForNewProperties(); - if (updatedLocal == null) { - throw Exception('Could not fetch local data for $fileName'); - } - - this.local = updatedLocal; - _didUpdateLocal = true; - return updatedLocal; - } - - Id id = Isar.autoIncrement; - - /// stores the raw SHA1 bytes as a base64 String - /// because Isar cannot sort lists of byte arrays - String checksum; - - String? thumbhash; - - @Index(unique: false, replace: false, type: IndexType.hash) - String? remoteId; - - @Index(unique: false, replace: false, type: IndexType.hash) - String? localId; - - @Index(unique: true, replace: false, composite: [CompositeIndex("checksum", type: IndexType.hash)]) - int ownerId; - - DateTime fileCreatedAt; - - DateTime fileModifiedAt; - - DateTime updatedAt; - - int durationInSeconds; - - @Enumerated(EnumType.ordinal) - AssetType type; - - short? width; - - short? height; - - String fileName; - - String? livePhotoVideoId; - - bool isFavorite; - - bool isArchived; - - bool isTrashed; - - bool isOffline; - - @ignore - ExifInfo? exifInfo; - - String? stackId; - - String? stackPrimaryAssetId; - - int stackCount; - - @Enumerated(EnumType.ordinal) - AssetVisibilityEnum visibility; - - /// Returns null if the asset has no sync access to the exif info - @ignore - double? get aspectRatio { - final orientatedWidth = this.orientatedWidth; - final orientatedHeight = this.orientatedHeight; - - if (orientatedWidth != null && orientatedHeight != null && orientatedWidth > 0 && orientatedHeight > 0) { - return orientatedWidth.toDouble() / orientatedHeight.toDouble(); - } - - return null; - } - - /// `true` if this [Asset] is present on the device - @ignore - bool get isLocal => localId != null; - - @ignore - bool get isInDb => id != Isar.autoIncrement; - - @ignore - String get name => p.withoutExtension(fileName); - - /// `true` if this [Asset] is present on the server - @ignore - bool get isRemote => remoteId != null; - - @ignore - bool get isImage => type == AssetType.image; - - @ignore - bool get isVideo => type == AssetType.video; - - @ignore - bool get isMotionPhoto => livePhotoVideoId != null; - - @ignore - AssetState get storage { - if (isRemote && isLocal) { - return AssetState.merged; - } else if (isRemote) { - return AssetState.remote; - } else if (isLocal) { - return AssetState.local; - } else { - throw Exception("Asset has illegal state: $this"); - } - } - - @ignore - Duration get duration => Duration(seconds: durationInSeconds); - - // ignore: invalid_annotation_target - @ignore - set byteHash(List hash) => checksum = base64.encode(hash); - - /// Returns null if the asset has no sync access to the exif info - @ignore - @pragma('vm:prefer-inline') - bool? get isFlipped { - final exifInfo = this.exifInfo; - if (exifInfo != null) { - return exifInfo.isFlipped; - } - - if (_didUpdateLocal && Platform.isAndroid) { - final local = this.local; - if (local == null) { - throw Exception('Asset $fileName has no local data'); - } - return local.orientation == 90 || local.orientation == 270; - } - - return null; - } - - /// Returns null if the asset has no sync access to the exif info - @ignore - @pragma('vm:prefer-inline') - int? get orientatedHeight { - final isFlipped = this.isFlipped; - if (isFlipped == null) { - return null; - } - - return isFlipped ? width : height; - } - - /// Returns null if the asset has no sync access to the exif info - @ignore - @pragma('vm:prefer-inline') - int? get orientatedWidth { - final isFlipped = this.isFlipped; - if (isFlipped == null) { - return null; - } - - return isFlipped ? height : width; - } - - @override - bool operator ==(other) { - if (other is! Asset) return false; - if (identical(this, other)) return true; - return id == other.id && - checksum == other.checksum && - remoteId == other.remoteId && - localId == other.localId && - ownerId == other.ownerId && - fileCreatedAt.isAtSameMomentAs(other.fileCreatedAt) && - fileModifiedAt.isAtSameMomentAs(other.fileModifiedAt) && - updatedAt.isAtSameMomentAs(other.updatedAt) && - durationInSeconds == other.durationInSeconds && - type == other.type && - width == other.width && - height == other.height && - fileName == other.fileName && - livePhotoVideoId == other.livePhotoVideoId && - isFavorite == other.isFavorite && - isLocal == other.isLocal && - isArchived == other.isArchived && - isTrashed == other.isTrashed && - stackCount == other.stackCount && - stackPrimaryAssetId == other.stackPrimaryAssetId && - stackId == other.stackId; - } - - @override - @ignore - int get hashCode => - id.hashCode ^ - checksum.hashCode ^ - remoteId.hashCode ^ - localId.hashCode ^ - ownerId.hashCode ^ - fileCreatedAt.hashCode ^ - fileModifiedAt.hashCode ^ - updatedAt.hashCode ^ - durationInSeconds.hashCode ^ - type.hashCode ^ - width.hashCode ^ - height.hashCode ^ - fileName.hashCode ^ - livePhotoVideoId.hashCode ^ - isFavorite.hashCode ^ - isLocal.hashCode ^ - isArchived.hashCode ^ - isTrashed.hashCode ^ - stackCount.hashCode ^ - stackPrimaryAssetId.hashCode ^ - stackId.hashCode; - - /// Returns `true` if this [Asset] can updated with values from parameter [a] - bool canUpdate(Asset a) { - assert(isInDb); - assert(checksum == a.checksum); - assert(a.storage != AssetState.merged); - return a.updatedAt.isAfter(updatedAt) || - a.isRemote && !isRemote || - a.isLocal && !isLocal || - width == null && a.width != null || - height == null && a.height != null || - livePhotoVideoId == null && a.livePhotoVideoId != null || - isFavorite != a.isFavorite || - isArchived != a.isArchived || - isTrashed != a.isTrashed || - isOffline != a.isOffline || - a.exifInfo?.latitude != exifInfo?.latitude || - a.exifInfo?.longitude != exifInfo?.longitude || - // no local stack count or different count from remote - a.thumbhash != thumbhash || - stackId != a.stackId || - stackCount != a.stackCount || - stackPrimaryAssetId == null && a.stackPrimaryAssetId != null || - visibility != a.visibility; - } - - /// Returns a new [Asset] with values from this and merged & updated with [a] - Asset updatedCopy(Asset a) { - assert(canUpdate(a)); - if (a.updatedAt.isAfter(updatedAt)) { - // take most values from newer asset - // keep vales that can never be set by the asset not in DB - if (a.isRemote) { - return a.copyWith( - id: id, - localId: localId, - width: a.width ?? width, - height: a.height ?? height, - exifInfo: a.exifInfo?.copyWith(assetId: id) ?? exifInfo, - ); - } else if (isRemote) { - return copyWith( - localId: localId ?? a.localId, - width: width ?? a.width, - height: height ?? a.height, - exifInfo: exifInfo ?? a.exifInfo?.copyWith(assetId: id), - ); - } else { - // TODO: Revisit this and remove all bool field assignments - return a.copyWith( - id: id, - remoteId: remoteId, - livePhotoVideoId: livePhotoVideoId, - // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app - // stack handling to properly handle it - stackId: stackId, - stackPrimaryAssetId: stackPrimaryAssetId == remoteId ? null : stackPrimaryAssetId, - stackCount: stackCount, - isFavorite: isFavorite, - isArchived: isArchived, - isTrashed: isTrashed, - isOffline: isOffline, - ); - } - } else { - // fill in potentially missing values, i.e. merge assets - if (a.isRemote) { - // values from remote take precedence - return copyWith( - remoteId: a.remoteId, - width: a.width, - height: a.height, - livePhotoVideoId: a.livePhotoVideoId, - // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app - // stack handling to properly handle it - stackId: a.stackId, - stackPrimaryAssetId: a.stackPrimaryAssetId == a.remoteId ? null : a.stackPrimaryAssetId, - stackCount: a.stackCount, - // isFavorite + isArchived are not set by device-only assets - isFavorite: a.isFavorite, - isArchived: a.isArchived, - isTrashed: a.isTrashed, - isOffline: a.isOffline, - exifInfo: a.exifInfo?.copyWith(assetId: id) ?? exifInfo, - thumbhash: a.thumbhash, - ); - } else { - // add only missing values (and set isLocal to true) - return copyWith( - localId: localId ?? a.localId, - width: width ?? a.width, - height: height ?? a.height, - exifInfo: exifInfo ?? a.exifInfo?.copyWith(assetId: id), // updated to use assetId - ); - } - } - } - - Asset copyWith({ - Id? id, - String? checksum, - String? remoteId, - String? localId, - int? ownerId, - DateTime? fileCreatedAt, - DateTime? fileModifiedAt, - DateTime? updatedAt, - int? durationInSeconds, - AssetType? type, - short? width, - short? height, - String? fileName, - String? livePhotoVideoId, - bool? isFavorite, - bool? isArchived, - bool? isTrashed, - bool? isOffline, - ExifInfo? exifInfo, - String? stackId, - String? stackPrimaryAssetId, - int? stackCount, - String? thumbhash, - AssetVisibilityEnum? visibility, - }) => Asset( - id: id ?? this.id, - checksum: checksum ?? this.checksum, - remoteId: remoteId ?? this.remoteId, - localId: localId ?? this.localId, - ownerId: ownerId ?? this.ownerId, - fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, - fileModifiedAt: fileModifiedAt ?? this.fileModifiedAt, - updatedAt: updatedAt ?? this.updatedAt, - durationInSeconds: durationInSeconds ?? this.durationInSeconds, - type: type ?? this.type, - width: width ?? this.width, - height: height ?? this.height, - fileName: fileName ?? this.fileName, - livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, - isFavorite: isFavorite ?? this.isFavorite, - isArchived: isArchived ?? this.isArchived, - isTrashed: isTrashed ?? this.isTrashed, - isOffline: isOffline ?? this.isOffline, - exifInfo: exifInfo ?? this.exifInfo, - stackId: stackId ?? this.stackId, - stackPrimaryAssetId: stackPrimaryAssetId ?? this.stackPrimaryAssetId, - stackCount: stackCount ?? this.stackCount, - thumbhash: thumbhash ?? this.thumbhash, - visibility: visibility ?? this.visibility, - ); - - Future put(Isar db) async { - await db.assets.put(this); - if (exifInfo != null) { - await db.exifInfos.put(entity.ExifInfo.fromDto(exifInfo!.copyWith(assetId: id))); - } - } - - static int compareById(Asset a, Asset b) => a.id.compareTo(b.id); - - static int compareByLocalId(Asset a, Asset b) => compareToNullable(a.localId, b.localId); - - static int compareByChecksum(Asset a, Asset b) => a.checksum.compareTo(b.checksum); - - static int compareByOwnerChecksum(Asset a, Asset b) { - final int ownerIdOrder = a.ownerId.compareTo(b.ownerId); - if (ownerIdOrder != 0) return ownerIdOrder; - return compareByChecksum(a, b); - } - - static int compareByOwnerChecksumCreatedModified(Asset a, Asset b) { - final int ownerIdOrder = a.ownerId.compareTo(b.ownerId); - if (ownerIdOrder != 0) return ownerIdOrder; - final int checksumOrder = compareByChecksum(a, b); - if (checksumOrder != 0) return checksumOrder; - final int createdOrder = a.fileCreatedAt.compareTo(b.fileCreatedAt); - if (createdOrder != 0) return createdOrder; - return a.fileModifiedAt.compareTo(b.fileModifiedAt); - } - - @override - String toString() { - return """ -{ - "id": ${id == Isar.autoIncrement ? '"N/A"' : id}, - "remoteId": "${remoteId ?? "N/A"}", - "localId": "${localId ?? "N/A"}", - "checksum": "$checksum", - "ownerId": $ownerId, - "livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}", - "stackId": "${stackId ?? "N/A"}", - "stackPrimaryAssetId": "${stackPrimaryAssetId ?? "N/A"}", - "stackCount": "$stackCount", - "fileCreatedAt": "$fileCreatedAt", - "fileModifiedAt": "$fileModifiedAt", - "updatedAt": "$updatedAt", - "durationInSeconds": $durationInSeconds, - "type": "$type", - "fileName": "$fileName", - "isFavorite": $isFavorite, - "isRemote": $isRemote, - "storage": "$storage", - "width": ${width ?? "N/A"}, - "height": ${height ?? "N/A"}, - "isArchived": $isArchived, - "isTrashed": $isTrashed, - "isOffline": $isOffline, - "visibility": "$visibility", -}"""; - } - - static getVisibility(AssetVisibility visibility) => switch (visibility) { - AssetVisibility.archive => AssetVisibilityEnum.archive, - AssetVisibility.hidden => AssetVisibilityEnum.hidden, - AssetVisibility.locked => AssetVisibilityEnum.locked, - AssetVisibility.timeline || _ => AssetVisibilityEnum.timeline, - }; -} - -enum AssetType { - // do not change this order! - other, - image, - video, - audio, -} - -extension AssetTypeEnumHelper on AssetTypeEnum { - AssetType toAssetType() => switch (this) { - AssetTypeEnum.IMAGE => AssetType.image, - AssetTypeEnum.VIDEO => AssetType.video, - AssetTypeEnum.AUDIO => AssetType.audio, - AssetTypeEnum.OTHER => AssetType.other, - _ => throw Exception(), - }; -} - -/// Describes where the information of this asset came from: -/// only from the local device, only from the remote server or merged from both -enum AssetState { local, remote, merged } - -extension AssetsHelper on IsarCollection { - Future deleteAllByRemoteId(Iterable ids) => ids.isEmpty ? Future.value(0) : remote(ids).deleteAll(); - Future deleteAllByLocalId(Iterable ids) => ids.isEmpty ? Future.value(0) : local(ids).deleteAll(); - Future> getAllByRemoteId(Iterable ids) => ids.isEmpty ? Future.value([]) : remote(ids).findAll(); - Future> getAllByLocalId(Iterable ids) => ids.isEmpty ? Future.value([]) : local(ids).findAll(); - Future getByRemoteId(String id) => where().remoteIdEqualTo(id).findFirst(); - - QueryBuilder remote(Iterable ids) => - where().anyOf(ids, (q, String e) => q.remoteIdEqualTo(e)); - QueryBuilder local(Iterable ids) { - return where().anyOf(ids, (q, String e) => q.localIdEqualTo(e)); - } -} diff --git a/mobile/lib/entities/asset.entity.g.dart b/mobile/lib/entities/asset.entity.g.dart deleted file mode 100644 index db6bc72331..0000000000 --- a/mobile/lib/entities/asset.entity.g.dart +++ /dev/null @@ -1,3711 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetAssetCollection on Isar { - IsarCollection get assets => this.collection(); -} - -const AssetSchema = CollectionSchema( - name: r'Asset', - id: -2933289051367723566, - properties: { - r'checksum': PropertySchema( - id: 0, - name: r'checksum', - type: IsarType.string, - ), - r'durationInSeconds': PropertySchema( - id: 1, - name: r'durationInSeconds', - type: IsarType.long, - ), - r'fileCreatedAt': PropertySchema( - id: 2, - name: r'fileCreatedAt', - type: IsarType.dateTime, - ), - r'fileModifiedAt': PropertySchema( - id: 3, - name: r'fileModifiedAt', - type: IsarType.dateTime, - ), - r'fileName': PropertySchema( - id: 4, - name: r'fileName', - type: IsarType.string, - ), - r'height': PropertySchema(id: 5, name: r'height', type: IsarType.int), - r'isArchived': PropertySchema( - id: 6, - name: r'isArchived', - type: IsarType.bool, - ), - r'isFavorite': PropertySchema( - id: 7, - name: r'isFavorite', - type: IsarType.bool, - ), - r'isOffline': PropertySchema( - id: 8, - name: r'isOffline', - type: IsarType.bool, - ), - r'isTrashed': PropertySchema( - id: 9, - name: r'isTrashed', - type: IsarType.bool, - ), - r'livePhotoVideoId': PropertySchema( - id: 10, - name: r'livePhotoVideoId', - type: IsarType.string, - ), - r'localId': PropertySchema(id: 11, name: r'localId', type: IsarType.string), - r'ownerId': PropertySchema(id: 12, name: r'ownerId', type: IsarType.long), - r'remoteId': PropertySchema( - id: 13, - name: r'remoteId', - type: IsarType.string, - ), - r'stackCount': PropertySchema( - id: 14, - name: r'stackCount', - type: IsarType.long, - ), - r'stackId': PropertySchema(id: 15, name: r'stackId', type: IsarType.string), - r'stackPrimaryAssetId': PropertySchema( - id: 16, - name: r'stackPrimaryAssetId', - type: IsarType.string, - ), - r'thumbhash': PropertySchema( - id: 17, - name: r'thumbhash', - type: IsarType.string, - ), - r'type': PropertySchema( - id: 18, - name: r'type', - type: IsarType.byte, - enumMap: _AssettypeEnumValueMap, - ), - r'updatedAt': PropertySchema( - id: 19, - name: r'updatedAt', - type: IsarType.dateTime, - ), - r'visibility': PropertySchema( - id: 20, - name: r'visibility', - type: IsarType.byte, - enumMap: _AssetvisibilityEnumValueMap, - ), - r'width': PropertySchema(id: 21, name: r'width', type: IsarType.int), - }, - - estimateSize: _assetEstimateSize, - serialize: _assetSerialize, - deserialize: _assetDeserialize, - deserializeProp: _assetDeserializeProp, - idName: r'id', - indexes: { - r'remoteId': IndexSchema( - id: 6301175856541681032, - name: r'remoteId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'remoteId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'localId': IndexSchema( - id: 1199848425898359622, - name: r'localId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'localId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'ownerId_checksum': IndexSchema( - id: -3295822444433175883, - name: r'ownerId_checksum', - unique: true, - replace: false, - properties: [ - IndexPropertySchema( - name: r'ownerId', - type: IndexType.value, - caseSensitive: false, - ), - IndexPropertySchema( - name: r'checksum', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _assetGetId, - getLinks: _assetGetLinks, - attach: _assetAttach, - version: '3.3.0-dev.3', -); - -int _assetEstimateSize( - Asset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.checksum.length * 3; - bytesCount += 3 + object.fileName.length * 3; - { - final value = object.livePhotoVideoId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.localId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.remoteId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.stackId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.stackPrimaryAssetId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.thumbhash; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _assetSerialize( - Asset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.checksum); - writer.writeLong(offsets[1], object.durationInSeconds); - writer.writeDateTime(offsets[2], object.fileCreatedAt); - writer.writeDateTime(offsets[3], object.fileModifiedAt); - writer.writeString(offsets[4], object.fileName); - writer.writeInt(offsets[5], object.height); - writer.writeBool(offsets[6], object.isArchived); - writer.writeBool(offsets[7], object.isFavorite); - writer.writeBool(offsets[8], object.isOffline); - writer.writeBool(offsets[9], object.isTrashed); - writer.writeString(offsets[10], object.livePhotoVideoId); - writer.writeString(offsets[11], object.localId); - writer.writeLong(offsets[12], object.ownerId); - writer.writeString(offsets[13], object.remoteId); - writer.writeLong(offsets[14], object.stackCount); - writer.writeString(offsets[15], object.stackId); - writer.writeString(offsets[16], object.stackPrimaryAssetId); - writer.writeString(offsets[17], object.thumbhash); - writer.writeByte(offsets[18], object.type.index); - writer.writeDateTime(offsets[19], object.updatedAt); - writer.writeByte(offsets[20], object.visibility.index); - writer.writeInt(offsets[21], object.width); -} - -Asset _assetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = Asset( - checksum: reader.readString(offsets[0]), - durationInSeconds: reader.readLong(offsets[1]), - fileCreatedAt: reader.readDateTime(offsets[2]), - fileModifiedAt: reader.readDateTime(offsets[3]), - fileName: reader.readString(offsets[4]), - height: reader.readIntOrNull(offsets[5]), - id: id, - isArchived: reader.readBoolOrNull(offsets[6]) ?? false, - isFavorite: reader.readBoolOrNull(offsets[7]) ?? false, - isOffline: reader.readBoolOrNull(offsets[8]) ?? false, - isTrashed: reader.readBoolOrNull(offsets[9]) ?? false, - livePhotoVideoId: reader.readStringOrNull(offsets[10]), - localId: reader.readStringOrNull(offsets[11]), - ownerId: reader.readLong(offsets[12]), - remoteId: reader.readStringOrNull(offsets[13]), - stackCount: reader.readLongOrNull(offsets[14]) ?? 0, - stackId: reader.readStringOrNull(offsets[15]), - stackPrimaryAssetId: reader.readStringOrNull(offsets[16]), - thumbhash: reader.readStringOrNull(offsets[17]), - type: - _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ?? - AssetType.other, - updatedAt: reader.readDateTime(offsets[19]), - visibility: - _AssetvisibilityValueEnumMap[reader.readByteOrNull(offsets[20])] ?? - AssetVisibilityEnum.timeline, - width: reader.readIntOrNull(offsets[21]), - ); - return object; -} - -P _assetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readLong(offset)) as P; - case 2: - return (reader.readDateTime(offset)) as P; - case 3: - return (reader.readDateTime(offset)) as P; - case 4: - return (reader.readString(offset)) as P; - case 5: - return (reader.readIntOrNull(offset)) as P; - case 6: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 7: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 8: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 9: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 10: - return (reader.readStringOrNull(offset)) as P; - case 11: - return (reader.readStringOrNull(offset)) as P; - case 12: - return (reader.readLong(offset)) as P; - case 13: - return (reader.readStringOrNull(offset)) as P; - case 14: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 15: - return (reader.readStringOrNull(offset)) as P; - case 16: - return (reader.readStringOrNull(offset)) as P; - case 17: - return (reader.readStringOrNull(offset)) as P; - case 18: - return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ?? - AssetType.other) - as P; - case 19: - return (reader.readDateTime(offset)) as P; - case 20: - return (_AssetvisibilityValueEnumMap[reader.readByteOrNull(offset)] ?? - AssetVisibilityEnum.timeline) - as P; - case 21: - return (reader.readIntOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _AssettypeEnumValueMap = {'other': 0, 'image': 1, 'video': 2, 'audio': 3}; -const _AssettypeValueEnumMap = { - 0: AssetType.other, - 1: AssetType.image, - 2: AssetType.video, - 3: AssetType.audio, -}; -const _AssetvisibilityEnumValueMap = { - 'timeline': 0, - 'hidden': 1, - 'archive': 2, - 'locked': 3, -}; -const _AssetvisibilityValueEnumMap = { - 0: AssetVisibilityEnum.timeline, - 1: AssetVisibilityEnum.hidden, - 2: AssetVisibilityEnum.archive, - 3: AssetVisibilityEnum.locked, -}; - -Id _assetGetId(Asset object) { - return object.id; -} - -List> _assetGetLinks(Asset object) { - return []; -} - -void _assetAttach(IsarCollection col, Id id, Asset object) { - object.id = id; -} - -extension AssetByIndex on IsarCollection { - Future getByOwnerIdChecksum(int ownerId, String checksum) { - return getByIndex(r'ownerId_checksum', [ownerId, checksum]); - } - - Asset? getByOwnerIdChecksumSync(int ownerId, String checksum) { - return getByIndexSync(r'ownerId_checksum', [ownerId, checksum]); - } - - Future deleteByOwnerIdChecksum(int ownerId, String checksum) { - return deleteByIndex(r'ownerId_checksum', [ownerId, checksum]); - } - - bool deleteByOwnerIdChecksumSync(int ownerId, String checksum) { - return deleteByIndexSync(r'ownerId_checksum', [ownerId, checksum]); - } - - Future> getAllByOwnerIdChecksum( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return getAllByIndex(r'ownerId_checksum', values); - } - - List getAllByOwnerIdChecksumSync( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return getAllByIndexSync(r'ownerId_checksum', values); - } - - Future deleteAllByOwnerIdChecksum( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return deleteAllByIndex(r'ownerId_checksum', values); - } - - int deleteAllByOwnerIdChecksumSync( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return deleteAllByIndexSync(r'ownerId_checksum', values); - } - - Future putByOwnerIdChecksum(Asset object) { - return putByIndex(r'ownerId_checksum', object); - } - - Id putByOwnerIdChecksumSync(Asset object, {bool saveLinks = true}) { - return putByIndexSync(r'ownerId_checksum', object, saveLinks: saveLinks); - } - - Future> putAllByOwnerIdChecksum(List objects) { - return putAllByIndex(r'ownerId_checksum', objects); - } - - List putAllByOwnerIdChecksumSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync( - r'ownerId_checksum', - objects, - saveLinks: saveLinks, - ); - } -} - -extension AssetQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension AssetQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [null]), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [remoteId]), - ); - }); - } - - QueryBuilder remoteIdNotEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [null]), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [localId]), - ); - }); - } - - QueryBuilder localIdNotEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder ownerIdEqualToAnyChecksum( - int ownerId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo( - indexName: r'ownerId_checksum', - value: [ownerId], - ), - ); - }); - } - - QueryBuilder ownerIdNotEqualToAnyChecksum( - int ownerId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [], - upper: [ownerId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [], - upper: [ownerId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder ownerIdGreaterThanAnyChecksum( - int ownerId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - includeLower: include, - upper: [], - ), - ); - }); - } - - QueryBuilder ownerIdLessThanAnyChecksum( - int ownerId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [], - upper: [ownerId], - includeUpper: include, - ), - ); - }); - } - - QueryBuilder ownerIdBetweenAnyChecksum( - int lowerOwnerId, - int upperOwnerId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [lowerOwnerId], - includeLower: includeLower, - upper: [upperOwnerId], - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder ownerIdChecksumEqualTo( - int ownerId, - String checksum, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo( - indexName: r'ownerId_checksum', - value: [ownerId, checksum], - ), - ); - }); - } - - QueryBuilder - ownerIdEqualToChecksumNotEqualTo(int ownerId, String checksum) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - upper: [ownerId, checksum], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId, checksum], - includeLower: false, - upper: [ownerId], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId, checksum], - includeLower: false, - upper: [ownerId], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - upper: [ownerId, checksum], - includeUpper: false, - ), - ); - } - }); - } -} - -extension AssetQueryFilter on QueryBuilder { - QueryBuilder checksumEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'checksum', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'checksum', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'checksum', value: ''), - ); - }); - } - - QueryBuilder checksumIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'checksum', value: ''), - ); - }); - } - - QueryBuilder durationInSecondsEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'durationInSeconds', value: value), - ); - }); - } - - QueryBuilder - durationInSecondsGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'durationInSeconds', - value: value, - ), - ); - }); - } - - QueryBuilder durationInSecondsLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'durationInSeconds', - value: value, - ), - ); - }); - } - - QueryBuilder durationInSecondsBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'durationInSeconds', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder fileCreatedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileCreatedAt', value: value), - ); - }); - } - - QueryBuilder fileCreatedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileCreatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileCreatedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileCreatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileCreatedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileCreatedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder fileModifiedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileModifiedAt', value: value), - ); - }); - } - - QueryBuilder fileModifiedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileModifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileModifiedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileModifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileModifiedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileModifiedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder fileNameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileName', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'fileName', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileName', value: ''), - ); - }); - } - - QueryBuilder fileNameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'fileName', value: ''), - ); - }); - } - - QueryBuilder heightIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'height'), - ); - }); - } - - QueryBuilder heightIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'height'), - ); - }); - } - - QueryBuilder heightEqualTo(int? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'height', value: value), - ); - }); - } - - QueryBuilder heightGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'height', - value: value, - ), - ); - }); - } - - QueryBuilder heightLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'height', - value: value, - ), - ); - }); - } - - QueryBuilder heightBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'height', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder isArchivedEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isArchived', value: value), - ); - }); - } - - QueryBuilder isFavoriteEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isFavorite', value: value), - ); - }); - } - - QueryBuilder isOfflineEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isOffline', value: value), - ); - }); - } - - QueryBuilder isTrashedEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isTrashed', value: value), - ); - }); - } - - QueryBuilder livePhotoVideoIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'livePhotoVideoId'), - ); - }); - } - - QueryBuilder - livePhotoVideoIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'livePhotoVideoId'), - ); - }); - } - - QueryBuilder livePhotoVideoIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'livePhotoVideoId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'livePhotoVideoId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'livePhotoVideoId', value: ''), - ); - }); - } - - QueryBuilder - livePhotoVideoIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'livePhotoVideoId', value: ''), - ); - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'localId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'localId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder localIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder ownerIdEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'ownerId', value: value), - ); - }); - } - - QueryBuilder ownerIdGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'ownerId', - value: value, - ), - ); - }); - } - - QueryBuilder ownerIdLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'ownerId', - value: value, - ), - ); - }); - } - - QueryBuilder ownerIdBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'ownerId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'remoteId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'remoteId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder remoteIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder stackCountEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'stackCount', value: value), - ); - }); - } - - QueryBuilder stackCountGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'stackCount', - value: value, - ), - ); - }); - } - - QueryBuilder stackCountLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'stackCount', - value: value, - ), - ); - }); - } - - QueryBuilder stackCountBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'stackCount', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder stackIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'stackId'), - ); - }); - } - - QueryBuilder stackIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'stackId'), - ); - }); - } - - QueryBuilder stackIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'stackId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'stackId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'stackId', value: ''), - ); - }); - } - - QueryBuilder stackIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'stackId', value: ''), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'stackPrimaryAssetId'), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'stackPrimaryAssetId'), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'stackPrimaryAssetId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'stackPrimaryAssetId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'stackPrimaryAssetId', value: ''), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - property: r'stackPrimaryAssetId', - value: '', - ), - ); - }); - } - - QueryBuilder thumbhashIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'thumbhash'), - ); - }); - } - - QueryBuilder thumbhashIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'thumbhash'), - ); - }); - } - - QueryBuilder thumbhashEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'thumbhash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'thumbhash', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'thumbhash', value: ''), - ); - }); - } - - QueryBuilder thumbhashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'thumbhash', value: ''), - ); - }); - } - - QueryBuilder typeEqualTo( - AssetType value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'type', value: value), - ); - }); - } - - QueryBuilder typeGreaterThan( - AssetType value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'type', - value: value, - ), - ); - }); - } - - QueryBuilder typeLessThan( - AssetType value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'type', - value: value, - ), - ); - }); - } - - QueryBuilder typeBetween( - AssetType lower, - AssetType upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'type', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder updatedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'updatedAt', value: value), - ); - }); - } - - QueryBuilder updatedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'updatedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder visibilityEqualTo( - AssetVisibilityEnum value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'visibility', value: value), - ); - }); - } - - QueryBuilder visibilityGreaterThan( - AssetVisibilityEnum value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'visibility', - value: value, - ), - ); - }); - } - - QueryBuilder visibilityLessThan( - AssetVisibilityEnum value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'visibility', - value: value, - ), - ); - }); - } - - QueryBuilder visibilityBetween( - AssetVisibilityEnum lower, - AssetVisibilityEnum upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'visibility', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder widthIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'width'), - ); - }); - } - - QueryBuilder widthIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'width'), - ); - }); - } - - QueryBuilder widthEqualTo(int? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'width', value: value), - ); - }); - } - - QueryBuilder widthGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'width', - value: value, - ), - ); - }); - } - - QueryBuilder widthLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'width', - value: value, - ), - ); - }); - } - - QueryBuilder widthBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'width', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension AssetQueryObject on QueryBuilder {} - -extension AssetQueryLinks on QueryBuilder {} - -extension AssetQuerySortBy on QueryBuilder { - QueryBuilder sortByChecksum() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.asc); - }); - } - - QueryBuilder sortByChecksumDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.desc); - }); - } - - QueryBuilder sortByDurationInSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.asc); - }); - } - - QueryBuilder sortByDurationInSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.desc); - }); - } - - QueryBuilder sortByFileCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.asc); - }); - } - - QueryBuilder sortByFileCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.desc); - }); - } - - QueryBuilder sortByFileModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.asc); - }); - } - - QueryBuilder sortByFileModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.desc); - }); - } - - QueryBuilder sortByFileName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.asc); - }); - } - - QueryBuilder sortByFileNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.desc); - }); - } - - QueryBuilder sortByHeight() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.asc); - }); - } - - QueryBuilder sortByHeightDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.desc); - }); - } - - QueryBuilder sortByIsArchived() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.asc); - }); - } - - QueryBuilder sortByIsArchivedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.desc); - }); - } - - QueryBuilder sortByIsFavorite() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.asc); - }); - } - - QueryBuilder sortByIsFavoriteDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.desc); - }); - } - - QueryBuilder sortByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.asc); - }); - } - - QueryBuilder sortByIsOfflineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.desc); - }); - } - - QueryBuilder sortByIsTrashed() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.asc); - }); - } - - QueryBuilder sortByIsTrashedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.desc); - }); - } - - QueryBuilder sortByLivePhotoVideoId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.asc); - }); - } - - QueryBuilder sortByLivePhotoVideoIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.desc); - }); - } - - QueryBuilder sortByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder sortByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder sortByOwnerId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.asc); - }); - } - - QueryBuilder sortByOwnerIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.desc); - }); - } - - QueryBuilder sortByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder sortByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder sortByStackCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.asc); - }); - } - - QueryBuilder sortByStackCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.desc); - }); - } - - QueryBuilder sortByStackId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.asc); - }); - } - - QueryBuilder sortByStackIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.desc); - }); - } - - QueryBuilder sortByStackPrimaryAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.asc); - }); - } - - QueryBuilder sortByStackPrimaryAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.desc); - }); - } - - QueryBuilder sortByThumbhash() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.asc); - }); - } - - QueryBuilder sortByThumbhashDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.desc); - }); - } - - QueryBuilder sortByType() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.asc); - }); - } - - QueryBuilder sortByTypeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.desc); - }); - } - - QueryBuilder sortByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder sortByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } - - QueryBuilder sortByVisibility() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.asc); - }); - } - - QueryBuilder sortByVisibilityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.desc); - }); - } - - QueryBuilder sortByWidth() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.asc); - }); - } - - QueryBuilder sortByWidthDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.desc); - }); - } -} - -extension AssetQuerySortThenBy on QueryBuilder { - QueryBuilder thenByChecksum() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.asc); - }); - } - - QueryBuilder thenByChecksumDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.desc); - }); - } - - QueryBuilder thenByDurationInSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.asc); - }); - } - - QueryBuilder thenByDurationInSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.desc); - }); - } - - QueryBuilder thenByFileCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.asc); - }); - } - - QueryBuilder thenByFileCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.desc); - }); - } - - QueryBuilder thenByFileModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.asc); - }); - } - - QueryBuilder thenByFileModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.desc); - }); - } - - QueryBuilder thenByFileName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.asc); - }); - } - - QueryBuilder thenByFileNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.desc); - }); - } - - QueryBuilder thenByHeight() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.asc); - }); - } - - QueryBuilder thenByHeightDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsArchived() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.asc); - }); - } - - QueryBuilder thenByIsArchivedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.desc); - }); - } - - QueryBuilder thenByIsFavorite() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.asc); - }); - } - - QueryBuilder thenByIsFavoriteDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.desc); - }); - } - - QueryBuilder thenByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.asc); - }); - } - - QueryBuilder thenByIsOfflineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.desc); - }); - } - - QueryBuilder thenByIsTrashed() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.asc); - }); - } - - QueryBuilder thenByIsTrashedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.desc); - }); - } - - QueryBuilder thenByLivePhotoVideoId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.asc); - }); - } - - QueryBuilder thenByLivePhotoVideoIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.desc); - }); - } - - QueryBuilder thenByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder thenByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder thenByOwnerId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.asc); - }); - } - - QueryBuilder thenByOwnerIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.desc); - }); - } - - QueryBuilder thenByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder thenByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder thenByStackCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.asc); - }); - } - - QueryBuilder thenByStackCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.desc); - }); - } - - QueryBuilder thenByStackId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.asc); - }); - } - - QueryBuilder thenByStackIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.desc); - }); - } - - QueryBuilder thenByStackPrimaryAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.asc); - }); - } - - QueryBuilder thenByStackPrimaryAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.desc); - }); - } - - QueryBuilder thenByThumbhash() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.asc); - }); - } - - QueryBuilder thenByThumbhashDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.desc); - }); - } - - QueryBuilder thenByType() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.asc); - }); - } - - QueryBuilder thenByTypeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.desc); - }); - } - - QueryBuilder thenByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder thenByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } - - QueryBuilder thenByVisibility() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.asc); - }); - } - - QueryBuilder thenByVisibilityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.desc); - }); - } - - QueryBuilder thenByWidth() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.asc); - }); - } - - QueryBuilder thenByWidthDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.desc); - }); - } -} - -extension AssetQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByChecksum({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'checksum', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByDurationInSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'durationInSeconds'); - }); - } - - QueryBuilder distinctByFileCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileCreatedAt'); - }); - } - - QueryBuilder distinctByFileModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileModifiedAt'); - }); - } - - QueryBuilder distinctByFileName({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileName', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByHeight() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'height'); - }); - } - - QueryBuilder distinctByIsArchived() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isArchived'); - }); - } - - QueryBuilder distinctByIsFavorite() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isFavorite'); - }); - } - - QueryBuilder distinctByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isOffline'); - }); - } - - QueryBuilder distinctByIsTrashed() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isTrashed'); - }); - } - - QueryBuilder distinctByLivePhotoVideoId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'livePhotoVideoId', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByLocalId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'localId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByOwnerId() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'ownerId'); - }); - } - - QueryBuilder distinctByRemoteId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'remoteId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByStackCount() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'stackCount'); - }); - } - - QueryBuilder distinctByStackId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'stackId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByStackPrimaryAssetId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'stackPrimaryAssetId', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByThumbhash({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'thumbhash', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByType() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'type'); - }); - } - - QueryBuilder distinctByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'updatedAt'); - }); - } - - QueryBuilder distinctByVisibility() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'visibility'); - }); - } - - QueryBuilder distinctByWidth() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'width'); - }); - } -} - -extension AssetQueryProperty on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder checksumProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'checksum'); - }); - } - - QueryBuilder durationInSecondsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'durationInSeconds'); - }); - } - - QueryBuilder fileCreatedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileCreatedAt'); - }); - } - - QueryBuilder fileModifiedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileModifiedAt'); - }); - } - - QueryBuilder fileNameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileName'); - }); - } - - QueryBuilder heightProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'height'); - }); - } - - QueryBuilder isArchivedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isArchived'); - }); - } - - QueryBuilder isFavoriteProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isFavorite'); - }); - } - - QueryBuilder isOfflineProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isOffline'); - }); - } - - QueryBuilder isTrashedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isTrashed'); - }); - } - - QueryBuilder livePhotoVideoIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'livePhotoVideoId'); - }); - } - - QueryBuilder localIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'localId'); - }); - } - - QueryBuilder ownerIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'ownerId'); - }); - } - - QueryBuilder remoteIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'remoteId'); - }); - } - - QueryBuilder stackCountProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackCount'); - }); - } - - QueryBuilder stackIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackId'); - }); - } - - QueryBuilder stackPrimaryAssetIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackPrimaryAssetId'); - }); - } - - QueryBuilder thumbhashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'thumbhash'); - }); - } - - QueryBuilder typeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'type'); - }); - } - - QueryBuilder updatedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'updatedAt'); - }); - } - - QueryBuilder - visibilityProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'visibility'); - }); - } - - QueryBuilder widthProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'width'); - }); - } -} diff --git a/mobile/lib/entities/backup_album.entity.dart b/mobile/lib/entities/backup_album.entity.dart deleted file mode 100644 index ad2a5d6718..0000000000 --- a/mobile/lib/entities/backup_album.entity.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'backup_album.entity.g.dart'; - -@Collection(inheritance: false) -class BackupAlbum { - String id; - DateTime lastBackup; - @Enumerated(EnumType.ordinal) - BackupSelection selection; - - BackupAlbum(this.id, this.lastBackup, this.selection); - - Id get isarId => fastHash(id); - - BackupAlbum copyWith({String? id, DateTime? lastBackup, BackupSelection? selection}) { - return BackupAlbum(id ?? this.id, lastBackup ?? this.lastBackup, selection ?? this.selection); - } -} - -enum BackupSelection { none, select, exclude } diff --git a/mobile/lib/entities/backup_album.entity.g.dart b/mobile/lib/entities/backup_album.entity.g.dart deleted file mode 100644 index 583aa55c4d..0000000000 --- a/mobile/lib/entities/backup_album.entity.g.dart +++ /dev/null @@ -1,679 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'backup_album.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetBackupAlbumCollection on Isar { - IsarCollection get backupAlbums => this.collection(); -} - -const BackupAlbumSchema = CollectionSchema( - name: r'BackupAlbum', - id: 8308487201128361847, - properties: { - r'id': PropertySchema(id: 0, name: r'id', type: IsarType.string), - r'lastBackup': PropertySchema( - id: 1, - name: r'lastBackup', - type: IsarType.dateTime, - ), - r'selection': PropertySchema( - id: 2, - name: r'selection', - type: IsarType.byte, - enumMap: _BackupAlbumselectionEnumValueMap, - ), - }, - - estimateSize: _backupAlbumEstimateSize, - serialize: _backupAlbumSerialize, - deserialize: _backupAlbumDeserialize, - deserializeProp: _backupAlbumDeserializeProp, - idName: r'isarId', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _backupAlbumGetId, - getLinks: _backupAlbumGetLinks, - attach: _backupAlbumAttach, - version: '3.3.0-dev.3', -); - -int _backupAlbumEstimateSize( - BackupAlbum object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _backupAlbumSerialize( - BackupAlbum object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.id); - writer.writeDateTime(offsets[1], object.lastBackup); - writer.writeByte(offsets[2], object.selection.index); -} - -BackupAlbum _backupAlbumDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = BackupAlbum( - reader.readString(offsets[0]), - reader.readDateTime(offsets[1]), - _BackupAlbumselectionValueEnumMap[reader.readByteOrNull(offsets[2])] ?? - BackupSelection.none, - ); - return object; -} - -P _backupAlbumDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readDateTime(offset)) as P; - case 2: - return (_BackupAlbumselectionValueEnumMap[reader.readByteOrNull( - offset, - )] ?? - BackupSelection.none) - as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _BackupAlbumselectionEnumValueMap = { - 'none': 0, - 'select': 1, - 'exclude': 2, -}; -const _BackupAlbumselectionValueEnumMap = { - 0: BackupSelection.none, - 1: BackupSelection.select, - 2: BackupSelection.exclude, -}; - -Id _backupAlbumGetId(BackupAlbum object) { - return object.isarId; -} - -List> _backupAlbumGetLinks(BackupAlbum object) { - return []; -} - -void _backupAlbumAttach( - IsarCollection col, - Id id, - BackupAlbum object, -) {} - -extension BackupAlbumQueryWhereSort - on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension BackupAlbumQueryWhere - on QueryBuilder { - QueryBuilder isarIdEqualTo( - Id isarId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder isarIdNotEqualTo( - Id isarId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder isarIdGreaterThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension BackupAlbumQueryFilter - on QueryBuilder { - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder isarIdEqualTo( - Id value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder - isarIdGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - lastBackupEqualTo(DateTime value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'lastBackup', value: value), - ); - }); - } - - QueryBuilder - lastBackupGreaterThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lastBackup', - value: value, - ), - ); - }); - } - - QueryBuilder - lastBackupLessThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lastBackup', - value: value, - ), - ); - }); - } - - QueryBuilder - lastBackupBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lastBackup', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - selectionEqualTo(BackupSelection value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'selection', value: value), - ); - }); - } - - QueryBuilder - selectionGreaterThan(BackupSelection value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'selection', - value: value, - ), - ); - }); - } - - QueryBuilder - selectionLessThan(BackupSelection value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'selection', - value: value, - ), - ); - }); - } - - QueryBuilder - selectionBetween( - BackupSelection lower, - BackupSelection upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'selection', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension BackupAlbumQueryObject - on QueryBuilder {} - -extension BackupAlbumQueryLinks - on QueryBuilder {} - -extension BackupAlbumQuerySortBy - on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder sortByLastBackup() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.asc); - }); - } - - QueryBuilder sortByLastBackupDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.desc); - }); - } - - QueryBuilder sortBySelection() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.asc); - }); - } - - QueryBuilder sortBySelectionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.desc); - }); - } -} - -extension BackupAlbumQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - - QueryBuilder thenByLastBackup() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.asc); - }); - } - - QueryBuilder thenByLastBackupDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.desc); - }); - } - - QueryBuilder thenBySelection() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.asc); - }); - } - - QueryBuilder thenBySelectionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.desc); - }); - } -} - -extension BackupAlbumQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByLastBackup() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lastBackup'); - }); - } - - QueryBuilder distinctBySelection() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'selection'); - }); - } -} - -extension BackupAlbumQueryProperty - on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder lastBackupProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lastBackup'); - }); - } - - QueryBuilder - selectionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'selection'); - }); - } -} diff --git a/mobile/lib/entities/device_asset.entity.dart b/mobile/lib/entities/device_asset.entity.dart deleted file mode 100644 index 0973dd4ff8..0000000000 --- a/mobile/lib/entities/device_asset.entity.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:isar/isar.dart'; - -class DeviceAsset { - DeviceAsset({required this.hash}); - - @Index(unique: false, type: IndexType.hash) - List hash; -} diff --git a/mobile/lib/entities/duplicated_asset.entity.dart b/mobile/lib/entities/duplicated_asset.entity.dart deleted file mode 100644 index 9368dc1a52..0000000000 --- a/mobile/lib/entities/duplicated_asset.entity.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'duplicated_asset.entity.g.dart'; - -@Collection(inheritance: false) -class DuplicatedAsset { - String id; - DuplicatedAsset(this.id); - Id get isarId => fastHash(id); -} diff --git a/mobile/lib/entities/duplicated_asset.entity.g.dart b/mobile/lib/entities/duplicated_asset.entity.g.dart deleted file mode 100644 index 80d2f344e6..0000000000 --- a/mobile/lib/entities/duplicated_asset.entity.g.dart +++ /dev/null @@ -1,444 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'duplicated_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetDuplicatedAssetCollection on Isar { - IsarCollection get duplicatedAssets => this.collection(); -} - -const DuplicatedAssetSchema = CollectionSchema( - name: r'DuplicatedAsset', - id: -2679334728174694496, - properties: { - r'id': PropertySchema(id: 0, name: r'id', type: IsarType.string), - }, - - estimateSize: _duplicatedAssetEstimateSize, - serialize: _duplicatedAssetSerialize, - deserialize: _duplicatedAssetDeserialize, - deserializeProp: _duplicatedAssetDeserializeProp, - idName: r'isarId', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _duplicatedAssetGetId, - getLinks: _duplicatedAssetGetLinks, - attach: _duplicatedAssetAttach, - version: '3.3.0-dev.3', -); - -int _duplicatedAssetEstimateSize( - DuplicatedAsset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _duplicatedAssetSerialize( - DuplicatedAsset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.id); -} - -DuplicatedAsset _duplicatedAssetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = DuplicatedAsset(reader.readString(offsets[0])); - return object; -} - -P _duplicatedAssetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _duplicatedAssetGetId(DuplicatedAsset object) { - return object.isarId; -} - -List> _duplicatedAssetGetLinks(DuplicatedAsset object) { - return []; -} - -void _duplicatedAssetAttach( - IsarCollection col, - Id id, - DuplicatedAsset object, -) {} - -extension DuplicatedAssetQueryWhereSort - on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension DuplicatedAssetQueryWhere - on QueryBuilder { - QueryBuilder - isarIdEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder - isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder - isarIdGreaterThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder - isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension DuplicatedAssetQueryFilter - on QueryBuilder { - QueryBuilder - idEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idLessThan(String value, {bool include = false, bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder - isarIdGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension DuplicatedAssetQueryObject - on QueryBuilder {} - -extension DuplicatedAssetQueryLinks - on QueryBuilder {} - -extension DuplicatedAssetQuerySortBy - on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension DuplicatedAssetQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder - thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } -} - -extension DuplicatedAssetQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } -} - -extension DuplicatedAssetQueryProperty - on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } -} diff --git a/mobile/lib/entities/etag.entity.dart b/mobile/lib/entities/etag.entity.dart deleted file mode 100644 index 3b8ef39c61..0000000000 --- a/mobile/lib/entities/etag.entity.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'etag.entity.g.dart'; - -@Collection(inheritance: false) -class ETag { - ETag({required this.id, this.assetCount, this.time}); - Id get isarId => fastHash(id); - @Index(unique: true, replace: true, type: IndexType.hash) - String id; - int? assetCount; - DateTime? time; -} diff --git a/mobile/lib/entities/etag.entity.g.dart b/mobile/lib/entities/etag.entity.g.dart deleted file mode 100644 index 03b4ea9918..0000000000 --- a/mobile/lib/entities/etag.entity.g.dart +++ /dev/null @@ -1,796 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'etag.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetETagCollection on Isar { - IsarCollection get eTags => this.collection(); -} - -const ETagSchema = CollectionSchema( - name: r'ETag', - id: -644290296585643859, - properties: { - r'assetCount': PropertySchema( - id: 0, - name: r'assetCount', - type: IsarType.long, - ), - r'id': PropertySchema(id: 1, name: r'id', type: IsarType.string), - r'time': PropertySchema(id: 2, name: r'time', type: IsarType.dateTime), - }, - - estimateSize: _eTagEstimateSize, - serialize: _eTagSerialize, - deserialize: _eTagDeserialize, - deserializeProp: _eTagDeserializeProp, - idName: r'isarId', - indexes: { - r'id': IndexSchema( - id: -3268401673993471357, - name: r'id', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'id', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _eTagGetId, - getLinks: _eTagGetLinks, - attach: _eTagAttach, - version: '3.3.0-dev.3', -); - -int _eTagEstimateSize( - ETag object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _eTagSerialize( - ETag object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeLong(offsets[0], object.assetCount); - writer.writeString(offsets[1], object.id); - writer.writeDateTime(offsets[2], object.time); -} - -ETag _eTagDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = ETag( - assetCount: reader.readLongOrNull(offsets[0]), - id: reader.readString(offsets[1]), - time: reader.readDateTimeOrNull(offsets[2]), - ); - return object; -} - -P _eTagDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readLongOrNull(offset)) as P; - case 1: - return (reader.readString(offset)) as P; - case 2: - return (reader.readDateTimeOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _eTagGetId(ETag object) { - return object.isarId; -} - -List> _eTagGetLinks(ETag object) { - return []; -} - -void _eTagAttach(IsarCollection col, Id id, ETag object) {} - -extension ETagByIndex on IsarCollection { - Future getById(String id) { - return getByIndex(r'id', [id]); - } - - ETag? getByIdSync(String id) { - return getByIndexSync(r'id', [id]); - } - - Future deleteById(String id) { - return deleteByIndex(r'id', [id]); - } - - bool deleteByIdSync(String id) { - return deleteByIndexSync(r'id', [id]); - } - - Future> getAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndex(r'id', values); - } - - List getAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'id', values); - } - - Future deleteAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'id', values); - } - - int deleteAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'id', values); - } - - Future putById(ETag object) { - return putByIndex(r'id', object); - } - - Id putByIdSync(ETag object, {bool saveLinks = true}) { - return putByIndexSync(r'id', object, saveLinks: saveLinks); - } - - Future> putAllById(List objects) { - return putAllByIndex(r'id', objects); - } - - List putAllByIdSync(List objects, {bool saveLinks = true}) { - return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); - } -} - -extension ETagQueryWhereSort on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension ETagQueryWhere on QueryBuilder { - QueryBuilder isarIdEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder isarIdGreaterThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'id', value: [id]), - ); - }); - } - - QueryBuilder idNotEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ); - } - }); - } -} - -extension ETagQueryFilter on QueryBuilder { - QueryBuilder assetCountIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'assetCount'), - ); - }); - } - - QueryBuilder assetCountIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'assetCount'), - ); - }); - } - - QueryBuilder assetCountEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'assetCount', value: value), - ); - }); - } - - QueryBuilder assetCountGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'assetCount', - value: value, - ), - ); - }); - } - - QueryBuilder assetCountLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'assetCount', - value: value, - ), - ); - }); - } - - QueryBuilder assetCountBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'assetCount', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder isarIdGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder timeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'time'), - ); - }); - } - - QueryBuilder timeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'time'), - ); - }); - } - - QueryBuilder timeEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'time', value: value), - ); - }); - } - - QueryBuilder timeGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'time', - value: value, - ), - ); - }); - } - - QueryBuilder timeLessThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'time', - value: value, - ), - ); - }); - } - - QueryBuilder timeBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'time', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension ETagQueryObject on QueryBuilder {} - -extension ETagQueryLinks on QueryBuilder {} - -extension ETagQuerySortBy on QueryBuilder { - QueryBuilder sortByAssetCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.asc); - }); - } - - QueryBuilder sortByAssetCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.desc); - }); - } - - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder sortByTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.asc); - }); - } - - QueryBuilder sortByTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.desc); - }); - } -} - -extension ETagQuerySortThenBy on QueryBuilder { - QueryBuilder thenByAssetCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.asc); - }); - } - - QueryBuilder thenByAssetCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - - QueryBuilder thenByTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.asc); - }); - } - - QueryBuilder thenByTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.desc); - }); - } -} - -extension ETagQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByAssetCount() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'assetCount'); - }); - } - - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByTime() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'time'); - }); - } -} - -extension ETagQueryProperty on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder assetCountProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'assetCount'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder timeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'time'); - }); - } -} diff --git a/mobile/lib/entities/ios_device_asset.entity.dart b/mobile/lib/entities/ios_device_asset.entity.dart deleted file mode 100644 index dfd0a660f8..0000000000 --- a/mobile/lib/entities/ios_device_asset.entity.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/entities/device_asset.entity.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'ios_device_asset.entity.g.dart'; - -@Collection() -class IOSDeviceAsset extends DeviceAsset { - IOSDeviceAsset({required this.id, required super.hash}); - - @Index(replace: true, unique: true, type: IndexType.hash) - String id; - Id get isarId => fastHash(id); -} diff --git a/mobile/lib/entities/ios_device_asset.entity.g.dart b/mobile/lib/entities/ios_device_asset.entity.g.dart deleted file mode 100644 index 252fe127bb..0000000000 --- a/mobile/lib/entities/ios_device_asset.entity.g.dart +++ /dev/null @@ -1,766 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'ios_device_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetIOSDeviceAssetCollection on Isar { - IsarCollection get iOSDeviceAssets => this.collection(); -} - -const IOSDeviceAssetSchema = CollectionSchema( - name: r'IOSDeviceAsset', - id: -1671546753821948030, - properties: { - r'hash': PropertySchema(id: 0, name: r'hash', type: IsarType.byteList), - r'id': PropertySchema(id: 1, name: r'id', type: IsarType.string), - }, - - estimateSize: _iOSDeviceAssetEstimateSize, - serialize: _iOSDeviceAssetSerialize, - deserialize: _iOSDeviceAssetDeserialize, - deserializeProp: _iOSDeviceAssetDeserializeProp, - idName: r'isarId', - indexes: { - r'id': IndexSchema( - id: -3268401673993471357, - name: r'id', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'id', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'hash': IndexSchema( - id: -7973251393006690288, - name: r'hash', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'hash', - type: IndexType.hash, - caseSensitive: false, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _iOSDeviceAssetGetId, - getLinks: _iOSDeviceAssetGetLinks, - attach: _iOSDeviceAssetAttach, - version: '3.3.0-dev.3', -); - -int _iOSDeviceAssetEstimateSize( - IOSDeviceAsset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.hash.length; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _iOSDeviceAssetSerialize( - IOSDeviceAsset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByteList(offsets[0], object.hash); - writer.writeString(offsets[1], object.id); -} - -IOSDeviceAsset _iOSDeviceAssetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = IOSDeviceAsset( - hash: reader.readByteList(offsets[0]) ?? [], - id: reader.readString(offsets[1]), - ); - return object; -} - -P _iOSDeviceAssetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readByteList(offset) ?? []) as P; - case 1: - return (reader.readString(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _iOSDeviceAssetGetId(IOSDeviceAsset object) { - return object.isarId; -} - -List> _iOSDeviceAssetGetLinks(IOSDeviceAsset object) { - return []; -} - -void _iOSDeviceAssetAttach( - IsarCollection col, - Id id, - IOSDeviceAsset object, -) {} - -extension IOSDeviceAssetByIndex on IsarCollection { - Future getById(String id) { - return getByIndex(r'id', [id]); - } - - IOSDeviceAsset? getByIdSync(String id) { - return getByIndexSync(r'id', [id]); - } - - Future deleteById(String id) { - return deleteByIndex(r'id', [id]); - } - - bool deleteByIdSync(String id) { - return deleteByIndexSync(r'id', [id]); - } - - Future> getAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndex(r'id', values); - } - - List getAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'id', values); - } - - Future deleteAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'id', values); - } - - int deleteAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'id', values); - } - - Future putById(IOSDeviceAsset object) { - return putByIndex(r'id', object); - } - - Id putByIdSync(IOSDeviceAsset object, {bool saveLinks = true}) { - return putByIndexSync(r'id', object, saveLinks: saveLinks); - } - - Future> putAllById(List objects) { - return putAllByIndex(r'id', objects); - } - - List putAllByIdSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); - } -} - -extension IOSDeviceAssetQueryWhereSort - on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension IOSDeviceAssetQueryWhere - on QueryBuilder { - QueryBuilder isarIdEqualTo( - Id isarId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder - isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder - isarIdGreaterThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo( - String id, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'id', value: [id]), - ); - }); - } - - QueryBuilder idNotEqualTo( - String id, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder hashEqualTo( - List hash, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'hash', value: [hash]), - ); - }); - } - - QueryBuilder - hashNotEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ); - } - }); - } -} - -extension IOSDeviceAssetQueryFilter - on QueryBuilder { - QueryBuilder - hashElementEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'hash', value: value), - ); - }); - } - - QueryBuilder - hashElementGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'hash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, true, length, true); - }); - } - - QueryBuilder - hashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, 0, true); - }); - } - - QueryBuilder - hashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, false, 999999, true); - }); - } - - QueryBuilder - hashLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, length, include); - }); - } - - QueryBuilder - hashLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, include, 999999, true); - }); - } - - QueryBuilder - hashLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'hash', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idLessThan(String value, {bool include = false, bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder - isarIdGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension IOSDeviceAssetQueryObject - on QueryBuilder {} - -extension IOSDeviceAssetQueryLinks - on QueryBuilder {} - -extension IOSDeviceAssetQuerySortBy - on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension IOSDeviceAssetQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder - thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } -} - -extension IOSDeviceAssetQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByHash() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hash'); - }); - } - - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } -} - -extension IOSDeviceAssetQueryProperty - on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder, QQueryOperations> hashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hash'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } -} diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index 7b59e119d6..17ad88cee9 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -1,38 +1,4 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; // ignore: non_constant_identifier_names final Store = StoreService.I; - -class SSLClientCertStoreVal { - final Uint8List data; - final String? password; - - const SSLClientCertStoreVal(this.data, this.password); - - Future save() async { - final b64Str = base64Encode(data); - await Store.put(StoreKey.sslClientCertData, b64Str); - if (password != null) { - await Store.put(StoreKey.sslClientPasswd, password!); - } - } - - static SSLClientCertStoreVal? load() { - final b64Str = Store.tryGet(StoreKey.sslClientCertData); - if (b64Str == null) { - return null; - } - final Uint8List certData = base64Decode(b64Str); - final passwd = Store.tryGet(StoreKey.sslClientPasswd); - return SSLClientCertStoreVal(certData, passwd); - } - - static Future delete() async { - await Store.delete(StoreKey.sslClientCertData); - await Store.delete(StoreKey.sslClientPasswd); - } -} diff --git a/mobile/lib/extensions/asset_extensions.dart b/mobile/lib/extensions/asset_extensions.dart index a8ca7ef2aa..6e8101bd04 100644 --- a/mobile/lib/extensions/asset_extensions.dart +++ b/mobile/lib/extensions/asset_extensions.dart @@ -1,17 +1,72 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/timezone.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/string_extensions.dart'; +import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; +import 'package:openapi/api.dart' as api; -extension TZExtension on Asset { - /// Returns the created time of the asset from the exif info (if available) or from - /// the fileCreatedAt field, adjusted to the timezone value from the exif info along with - /// the timezone offset in [Duration] - (DateTime, Duration) getTZAdjustedTimeAndOffset() { - DateTime dt = fileCreatedAt.toLocal(); +extension DTOToAsset on api.AssetResponseDto { + RemoteAsset toDto() { + return RemoteAsset( + id: id, + name: originalFileName, + checksum: checksum, + createdAt: fileCreatedAt, + updatedAt: updatedAt, + ownerId: ownerId, + visibility: visibility.toAssetVisibility(), + durationInSeconds: duration?.toDuration()?.inSeconds ?? 0, + height: height?.toInt(), + width: width?.toInt(), + isFavorite: isFavorite, + livePhotoVideoId: livePhotoVideoId, + thumbHash: thumbhash, + localId: null, + type: type.toAssetType(), + stackId: stack?.id, + isEdited: isEdited, + ); + } - if (exifInfo?.dateTimeOriginal != null) { - return applyTimezoneOffset(dateTime: exifInfo!.dateTimeOriginal!, timeZone: exifInfo?.timeZone); - } - - return (dt, dt.timeZoneOffset); + RemoteAssetExif toDtoWithExif() { + return RemoteAssetExif( + id: id, + name: originalFileName, + checksum: checksum, + createdAt: fileCreatedAt, + updatedAt: updatedAt, + ownerId: ownerId, + visibility: visibility.toAssetVisibility(), + durationInSeconds: duration?.toDuration()?.inSeconds ?? 0, + height: height?.toInt(), + width: width?.toInt(), + isFavorite: isFavorite, + livePhotoVideoId: livePhotoVideoId, + thumbHash: thumbhash, + localId: null, + type: type.toAssetType(), + stackId: stack?.id, + isEdited: isEdited, + exifInfo: exifInfo != null ? ExifDtoConverter.fromDto(exifInfo!) : const ExifInfo(), + ); } } + +extension on api.AssetVisibility { + AssetVisibility toAssetVisibility() => switch (this) { + api.AssetVisibility.timeline => AssetVisibility.timeline, + api.AssetVisibility.hidden => AssetVisibility.hidden, + api.AssetVisibility.archive => AssetVisibility.archive, + api.AssetVisibility.locked => AssetVisibility.locked, + _ => AssetVisibility.timeline, + }; +} + +extension on api.AssetTypeEnum { + AssetType toAssetType() => switch (this) { + api.AssetTypeEnum.IMAGE => AssetType.image, + api.AssetTypeEnum.VIDEO => AssetType.video, + api.AssetTypeEnum.AUDIO => AssetType.audio, + api.AssetTypeEnum.OTHER => AssetType.other, + _ => throw Exception('Unknown AssetType value: $this'), + }; +} diff --git a/mobile/lib/extensions/collection_extensions.dart b/mobile/lib/extensions/collection_extensions.dart index 541db7ccaf..b861eb0570 100644 --- a/mobile/lib/extensions/collection_extensions.dart +++ b/mobile/lib/extensions/collection_extensions.dart @@ -1,9 +1,6 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/hash.dart'; extension ListExtension on List { List uniqueConsecutive({int Function(E a, E b)? compare, void Function(E a, E b)? onDuplicate}) { @@ -40,31 +37,6 @@ extension IntListExtension on Iterable { } } -extension AssetListExtension on Iterable { - /// Returns the assets that are already available in the Immich server - Iterable remoteOnly({void Function()? errorCallback}) { - final bool onlyRemote = every((e) => e.isRemote); - if (!onlyRemote) { - if (errorCallback != null) errorCallback(); - return where((a) => a.isRemote); - } - return this; - } - - /// Returns the assets that are owned by the user passed to the [owner] param - /// If [owner] is null, an empty list is returned - Iterable ownedOnly(UserDto? owner, {void Function()? errorCallback}) { - if (owner == null) return []; - final isarUserId = fastHash(owner.id); - final bool onlyOwned = every((e) => e.ownerId == isarUserId); - if (!onlyOwned) { - if (errorCallback != null) errorCallback(); - return where((a) => a.ownerId == isarUserId); - } - return this; - } -} - extension SortedByProperty on Iterable { Iterable sortedByField(Comparable Function(T e) key) { return sorted((a, b) => key(a).compareTo(key(b))); diff --git a/mobile/lib/extensions/object_extensions.dart b/mobile/lib/extensions/object_extensions.dart new file mode 100644 index 0000000000..4e76532137 --- /dev/null +++ b/mobile/lib/extensions/object_extensions.dart @@ -0,0 +1,3 @@ +extension Let on T { + R let(R Function(T) transform) => transform(this); +} diff --git a/mobile/lib/extensions/translate_extensions.dart b/mobile/lib/extensions/translate_extensions.dart index 7677f3cbd8..b01203a90c 100644 --- a/mobile/lib/extensions/translate_extensions.dart +++ b/mobile/lib/extensions/translate_extensions.dart @@ -1,7 +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'; +import 'package:intl/message_format.dart'; extension StringTranslateExtension on String { String t({BuildContext? context, Map? args}) { diff --git a/mobile/lib/infrastructure/entities/asset_edit.entity.dart b/mobile/lib/infrastructure/entities/asset_edit.entity.dart index 22d059bdb4..87a05ab8fe 100644 --- a/mobile/lib/infrastructure/entities/asset_edit.entity.dart +++ b/mobile/lib/infrastructure/entities/asset_edit.entity.dart @@ -1,8 +1,10 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/extensions/object_extensions.dart'; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +import 'package:openapi/api.dart' hide AssetEditAction; @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)') class AssetEditEntity extends Table with DriftDefaultsMixin { @@ -27,7 +29,12 @@ final JsonTypeConverter2, Uint8List, Object?> editParameter ); extension AssetEditEntityDataDomainEx on AssetEditEntityData { - AssetEdit toDto() { - return AssetEdit(action: action, parameters: parameters); + AssetEdit? toDto() { + return switch (action) { + AssetEditAction.crop => CropParameters.fromJson(parameters)?.let(CropEdit.new), + AssetEditAction.rotate => RotateParameters.fromJson(parameters)?.let(RotateEdit.new), + AssetEditAction.mirror => MirrorParameters.fromJson(parameters)?.let(MirrorEdit.new), + AssetEditAction.other => null, + }; } } diff --git a/mobile/lib/infrastructure/entities/device_asset.entity.dart b/mobile/lib/infrastructure/entities/device_asset.entity.dart deleted file mode 100644 index e3e4a0d4f4..0000000000 --- a/mobile/lib/infrastructure/entities/device_asset.entity.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:typed_data'; - -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'device_asset.entity.g.dart'; - -@Collection(inheritance: false) -class DeviceAssetEntity { - Id get id => fastHash(assetId); - - @Index(replace: true, unique: true, type: IndexType.hash) - final String assetId; - @Index(unique: false, type: IndexType.hash) - final List hash; - final DateTime modifiedTime; - - const DeviceAssetEntity({required this.assetId, required this.hash, required this.modifiedTime}); - - DeviceAsset toModel() => DeviceAsset(assetId: assetId, hash: Uint8List.fromList(hash), modifiedTime: modifiedTime); - - static DeviceAssetEntity fromDto(DeviceAsset dto) => - DeviceAssetEntity(assetId: dto.assetId, hash: dto.hash, modifiedTime: dto.modifiedTime); -} diff --git a/mobile/lib/infrastructure/entities/device_asset.entity.g.dart b/mobile/lib/infrastructure/entities/device_asset.entity.g.dart deleted file mode 100644 index b6c30aca6f..0000000000 --- a/mobile/lib/infrastructure/entities/device_asset.entity.g.dart +++ /dev/null @@ -1,874 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'device_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetDeviceAssetEntityCollection on Isar { - IsarCollection get deviceAssetEntitys => this.collection(); -} - -const DeviceAssetEntitySchema = CollectionSchema( - name: r'DeviceAssetEntity', - id: 6967030785073446271, - properties: { - r'assetId': PropertySchema(id: 0, name: r'assetId', type: IsarType.string), - r'hash': PropertySchema(id: 1, name: r'hash', type: IsarType.byteList), - r'modifiedTime': PropertySchema( - id: 2, - name: r'modifiedTime', - type: IsarType.dateTime, - ), - }, - - estimateSize: _deviceAssetEntityEstimateSize, - serialize: _deviceAssetEntitySerialize, - deserialize: _deviceAssetEntityDeserialize, - deserializeProp: _deviceAssetEntityDeserializeProp, - idName: r'id', - indexes: { - r'assetId': IndexSchema( - id: 174362542210192109, - name: r'assetId', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'assetId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'hash': IndexSchema( - id: -7973251393006690288, - name: r'hash', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'hash', - type: IndexType.hash, - caseSensitive: false, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _deviceAssetEntityGetId, - getLinks: _deviceAssetEntityGetLinks, - attach: _deviceAssetEntityAttach, - version: '3.3.0-dev.3', -); - -int _deviceAssetEntityEstimateSize( - DeviceAssetEntity object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.assetId.length * 3; - bytesCount += 3 + object.hash.length; - return bytesCount; -} - -void _deviceAssetEntitySerialize( - DeviceAssetEntity object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.assetId); - writer.writeByteList(offsets[1], object.hash); - writer.writeDateTime(offsets[2], object.modifiedTime); -} - -DeviceAssetEntity _deviceAssetEntityDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = DeviceAssetEntity( - assetId: reader.readString(offsets[0]), - hash: reader.readByteList(offsets[1]) ?? [], - modifiedTime: reader.readDateTime(offsets[2]), - ); - return object; -} - -P _deviceAssetEntityDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readByteList(offset) ?? []) as P; - case 2: - return (reader.readDateTime(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _deviceAssetEntityGetId(DeviceAssetEntity object) { - return object.id; -} - -List> _deviceAssetEntityGetLinks( - DeviceAssetEntity object, -) { - return []; -} - -void _deviceAssetEntityAttach( - IsarCollection col, - Id id, - DeviceAssetEntity object, -) {} - -extension DeviceAssetEntityByIndex on IsarCollection { - Future getByAssetId(String assetId) { - return getByIndex(r'assetId', [assetId]); - } - - DeviceAssetEntity? getByAssetIdSync(String assetId) { - return getByIndexSync(r'assetId', [assetId]); - } - - Future deleteByAssetId(String assetId) { - return deleteByIndex(r'assetId', [assetId]); - } - - bool deleteByAssetIdSync(String assetId) { - return deleteByIndexSync(r'assetId', [assetId]); - } - - Future> getAllByAssetId(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return getAllByIndex(r'assetId', values); - } - - List getAllByAssetIdSync(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'assetId', values); - } - - Future deleteAllByAssetId(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'assetId', values); - } - - int deleteAllByAssetIdSync(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'assetId', values); - } - - Future putByAssetId(DeviceAssetEntity object) { - return putByIndex(r'assetId', object); - } - - Id putByAssetIdSync(DeviceAssetEntity object, {bool saveLinks = true}) { - return putByIndexSync(r'assetId', object, saveLinks: saveLinks); - } - - Future> putAllByAssetId(List objects) { - return putAllByIndex(r'assetId', objects); - } - - List putAllByAssetIdSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync(r'assetId', objects, saveLinks: saveLinks); - } -} - -extension DeviceAssetEntityQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension DeviceAssetEntityQueryWhere - on QueryBuilder { - QueryBuilder - idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder - idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder - idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder - idLessThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder - idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - assetIdEqualTo(String assetId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'assetId', value: [assetId]), - ); - }); - } - - QueryBuilder - assetIdNotEqualTo(String assetId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [], - upper: [assetId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [assetId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [assetId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [], - upper: [assetId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder - hashEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'hash', value: [hash]), - ); - }); - } - - QueryBuilder - hashNotEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ); - } - }); - } -} - -extension DeviceAssetEntityQueryFilter - on QueryBuilder { - QueryBuilder - assetIdEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'assetId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'assetId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'assetId', value: ''), - ); - }); - } - - QueryBuilder - assetIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'assetId', value: ''), - ); - }); - } - - QueryBuilder - hashElementEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'hash', value: value), - ); - }); - } - - QueryBuilder - hashElementGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'hash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, true, length, true); - }); - } - - QueryBuilder - hashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, 0, true); - }); - } - - QueryBuilder - hashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, false, 999999, true); - }); - } - - QueryBuilder - hashLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, length, include); - }); - } - - QueryBuilder - hashLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, include, 999999, true); - }); - } - - QueryBuilder - hashLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'hash', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder - idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder - idGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - modifiedTimeEqualTo(DateTime value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'modifiedTime', value: value), - ); - }); - } - - QueryBuilder - modifiedTimeGreaterThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'modifiedTime', - value: value, - ), - ); - }); - } - - QueryBuilder - modifiedTimeLessThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'modifiedTime', - value: value, - ), - ); - }); - } - - QueryBuilder - modifiedTimeBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'modifiedTime', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension DeviceAssetEntityQueryObject - on QueryBuilder {} - -extension DeviceAssetEntityQueryLinks - on QueryBuilder {} - -extension DeviceAssetEntityQuerySortBy - on QueryBuilder { - QueryBuilder - sortByAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.asc); - }); - } - - QueryBuilder - sortByAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.desc); - }); - } - - QueryBuilder - sortByModifiedTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.asc); - }); - } - - QueryBuilder - sortByModifiedTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.desc); - }); - } -} - -extension DeviceAssetEntityQuerySortThenBy - on QueryBuilder { - QueryBuilder - thenByAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.asc); - }); - } - - QueryBuilder - thenByAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder - thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder - thenByModifiedTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.asc); - }); - } - - QueryBuilder - thenByModifiedTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.desc); - }); - } -} - -extension DeviceAssetEntityQueryWhereDistinct - on QueryBuilder { - QueryBuilder - distinctByAssetId({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'assetId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder - distinctByHash() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hash'); - }); - } - - QueryBuilder - distinctByModifiedTime() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'modifiedTime'); - }); - } -} - -extension DeviceAssetEntityQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder assetIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'assetId'); - }); - } - - QueryBuilder, QQueryOperations> hashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hash'); - }); - } - - QueryBuilder - modifiedTimeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'modifiedTime'); - }); - } -} diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 77cae5dbbe..e009029ea7 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -4,96 +4,6 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:isar/isar.dart'; - -part 'exif.entity.g.dart'; - -/// Exif information 1:1 relation with Asset -@Collection(inheritance: false) -class ExifInfo { - final Id? id; - final int? fileSize; - final DateTime? dateTimeOriginal; - final String? timeZone; - final String? make; - final String? model; - final String? lens; - final float? f; - final float? mm; - final short? iso; - final float? exposureSeconds; - final float? lat; - final float? long; - final String? city; - final String? state; - final String? country; - final String? description; - final String? orientation; - - const ExifInfo({ - this.id, - this.fileSize, - this.dateTimeOriginal, - this.timeZone, - this.make, - this.model, - this.lens, - this.f, - this.mm, - this.iso, - this.exposureSeconds, - this.lat, - this.long, - this.city, - this.state, - this.country, - this.description, - this.orientation, - }); - - static ExifInfo fromDto(domain.ExifInfo dto) => ExifInfo( - id: dto.assetId, - fileSize: dto.fileSize, - dateTimeOriginal: dto.dateTimeOriginal, - timeZone: dto.timeZone, - make: dto.make, - model: dto.model, - lens: dto.lens, - f: dto.f, - mm: dto.mm, - iso: dto.iso?.toInt(), - exposureSeconds: dto.exposureSeconds, - lat: dto.latitude, - long: dto.longitude, - city: dto.city, - state: dto.state, - country: dto.country, - description: dto.description, - orientation: dto.orientation, - ); - - domain.ExifInfo toDto() => domain.ExifInfo( - assetId: id, - fileSize: fileSize, - description: description, - orientation: orientation, - timeZone: timeZone, - dateTimeOriginal: dateTimeOriginal, - isFlipped: ExifDtoConverter.isOrientationFlipped(orientation), - latitude: lat, - longitude: long, - city: city, - state: state, - country: country, - make: make, - model: model, - lens: lens, - f: f, - mm: mm, - iso: iso?.toInt(), - exposureSeconds: exposureSeconds, - ); -} @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)') class RemoteExifEntity extends Table with DriftDefaultsMixin { @@ -152,6 +62,8 @@ extension RemoteExifEntityDataDomainEx on RemoteExifEntityData { fileSize: fileSize, dateTimeOriginal: dateTimeOriginal, rating: rating, + width: width, + height: height, timeZone: timeZone, make: make, model: model, diff --git a/mobile/lib/infrastructure/entities/exif.entity.g.dart b/mobile/lib/infrastructure/entities/exif.entity.g.dart deleted file mode 100644 index ffbfd0d8f0..0000000000 --- a/mobile/lib/infrastructure/entities/exif.entity.g.dart +++ /dev/null @@ -1,3200 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'exif.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetExifInfoCollection on Isar { - IsarCollection get exifInfos => this.collection(); -} - -const ExifInfoSchema = CollectionSchema( - name: r'ExifInfo', - id: -2409260054350835217, - properties: { - r'city': PropertySchema(id: 0, name: r'city', type: IsarType.string), - r'country': PropertySchema(id: 1, name: r'country', type: IsarType.string), - r'dateTimeOriginal': PropertySchema( - id: 2, - name: r'dateTimeOriginal', - type: IsarType.dateTime, - ), - r'description': PropertySchema( - id: 3, - name: r'description', - type: IsarType.string, - ), - r'exposureSeconds': PropertySchema( - id: 4, - name: r'exposureSeconds', - type: IsarType.float, - ), - r'f': PropertySchema(id: 5, name: r'f', type: IsarType.float), - r'fileSize': PropertySchema(id: 6, name: r'fileSize', type: IsarType.long), - r'iso': PropertySchema(id: 7, name: r'iso', type: IsarType.int), - r'lat': PropertySchema(id: 8, name: r'lat', type: IsarType.float), - r'lens': PropertySchema(id: 9, name: r'lens', type: IsarType.string), - r'long': PropertySchema(id: 10, name: r'long', type: IsarType.float), - r'make': PropertySchema(id: 11, name: r'make', type: IsarType.string), - r'mm': PropertySchema(id: 12, name: r'mm', type: IsarType.float), - r'model': PropertySchema(id: 13, name: r'model', type: IsarType.string), - r'orientation': PropertySchema( - id: 14, - name: r'orientation', - type: IsarType.string, - ), - r'state': PropertySchema(id: 15, name: r'state', type: IsarType.string), - r'timeZone': PropertySchema( - id: 16, - name: r'timeZone', - type: IsarType.string, - ), - }, - - estimateSize: _exifInfoEstimateSize, - serialize: _exifInfoSerialize, - deserialize: _exifInfoDeserialize, - deserializeProp: _exifInfoDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _exifInfoGetId, - getLinks: _exifInfoGetLinks, - attach: _exifInfoAttach, - version: '3.3.0-dev.3', -); - -int _exifInfoEstimateSize( - ExifInfo object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.city; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.country; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.description; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.lens; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.make; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.model; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.orientation; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.state; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.timeZone; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _exifInfoSerialize( - ExifInfo object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.city); - writer.writeString(offsets[1], object.country); - writer.writeDateTime(offsets[2], object.dateTimeOriginal); - writer.writeString(offsets[3], object.description); - writer.writeFloat(offsets[4], object.exposureSeconds); - writer.writeFloat(offsets[5], object.f); - writer.writeLong(offsets[6], object.fileSize); - writer.writeInt(offsets[7], object.iso); - writer.writeFloat(offsets[8], object.lat); - writer.writeString(offsets[9], object.lens); - writer.writeFloat(offsets[10], object.long); - writer.writeString(offsets[11], object.make); - writer.writeFloat(offsets[12], object.mm); - writer.writeString(offsets[13], object.model); - writer.writeString(offsets[14], object.orientation); - writer.writeString(offsets[15], object.state); - writer.writeString(offsets[16], object.timeZone); -} - -ExifInfo _exifInfoDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = ExifInfo( - city: reader.readStringOrNull(offsets[0]), - country: reader.readStringOrNull(offsets[1]), - dateTimeOriginal: reader.readDateTimeOrNull(offsets[2]), - description: reader.readStringOrNull(offsets[3]), - exposureSeconds: reader.readFloatOrNull(offsets[4]), - f: reader.readFloatOrNull(offsets[5]), - fileSize: reader.readLongOrNull(offsets[6]), - id: id, - iso: reader.readIntOrNull(offsets[7]), - lat: reader.readFloatOrNull(offsets[8]), - lens: reader.readStringOrNull(offsets[9]), - long: reader.readFloatOrNull(offsets[10]), - make: reader.readStringOrNull(offsets[11]), - mm: reader.readFloatOrNull(offsets[12]), - model: reader.readStringOrNull(offsets[13]), - orientation: reader.readStringOrNull(offsets[14]), - state: reader.readStringOrNull(offsets[15]), - timeZone: reader.readStringOrNull(offsets[16]), - ); - return object; -} - -P _exifInfoDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readStringOrNull(offset)) as P; - case 1: - return (reader.readStringOrNull(offset)) as P; - case 2: - return (reader.readDateTimeOrNull(offset)) as P; - case 3: - return (reader.readStringOrNull(offset)) as P; - case 4: - return (reader.readFloatOrNull(offset)) as P; - case 5: - return (reader.readFloatOrNull(offset)) as P; - case 6: - return (reader.readLongOrNull(offset)) as P; - case 7: - return (reader.readIntOrNull(offset)) as P; - case 8: - return (reader.readFloatOrNull(offset)) as P; - case 9: - return (reader.readStringOrNull(offset)) as P; - case 10: - return (reader.readFloatOrNull(offset)) as P; - case 11: - return (reader.readStringOrNull(offset)) as P; - case 12: - return (reader.readFloatOrNull(offset)) as P; - case 13: - return (reader.readStringOrNull(offset)) as P; - case 14: - return (reader.readStringOrNull(offset)) as P; - case 15: - return (reader.readStringOrNull(offset)) as P; - case 16: - return (reader.readStringOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _exifInfoGetId(ExifInfo object) { - return object.id ?? Isar.autoIncrement; -} - -List> _exifInfoGetLinks(ExifInfo object) { - return []; -} - -void _exifInfoAttach(IsarCollection col, Id id, ExifInfo object) {} - -extension ExifInfoQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension ExifInfoQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension ExifInfoQueryFilter - on QueryBuilder { - QueryBuilder cityIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'city'), - ); - }); - } - - QueryBuilder cityIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'city'), - ); - }); - } - - QueryBuilder cityEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'city', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'city', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'city', value: ''), - ); - }); - } - - QueryBuilder cityIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'city', value: ''), - ); - }); - } - - QueryBuilder countryIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'country'), - ); - }); - } - - QueryBuilder countryIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'country'), - ); - }); - } - - QueryBuilder countryEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'country', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'country', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'country', value: ''), - ); - }); - } - - QueryBuilder countryIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'country', value: ''), - ); - }); - } - - QueryBuilder - dateTimeOriginalIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'dateTimeOriginal'), - ); - }); - } - - QueryBuilder - dateTimeOriginalIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'dateTimeOriginal'), - ); - }); - } - - QueryBuilder - dateTimeOriginalEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'dateTimeOriginal', value: value), - ); - }); - } - - QueryBuilder - dateTimeOriginalGreaterThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'dateTimeOriginal', - value: value, - ), - ); - }); - } - - QueryBuilder - dateTimeOriginalLessThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'dateTimeOriginal', - value: value, - ), - ); - }); - } - - QueryBuilder - dateTimeOriginalBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'dateTimeOriginal', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder descriptionIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'description'), - ); - }); - } - - QueryBuilder - descriptionIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'description'), - ); - }); - } - - QueryBuilder descriptionEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - descriptionGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'description', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'description', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'description', value: ''), - ); - }); - } - - QueryBuilder - descriptionIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'description', value: ''), - ); - }); - } - - QueryBuilder - exposureSecondsIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'exposureSeconds'), - ); - }); - } - - QueryBuilder - exposureSecondsIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'exposureSeconds'), - ); - }); - } - - QueryBuilder - exposureSecondsEqualTo(double? value, {double epsilon = Query.epsilon}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'exposureSeconds', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - exposureSecondsGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'exposureSeconds', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - exposureSecondsLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'exposureSeconds', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - exposureSecondsBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'exposureSeconds', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'f'), - ); - }); - } - - QueryBuilder fIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'f'), - ); - }); - } - - QueryBuilder fEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'f', value: value, epsilon: epsilon), - ); - }); - } - - QueryBuilder fGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'f', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'f', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'f', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fileSizeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'fileSize'), - ); - }); - } - - QueryBuilder fileSizeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'fileSize'), - ); - }); - } - - QueryBuilder fileSizeEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileSize', value: value), - ); - }); - } - - QueryBuilder fileSizeGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileSize', - value: value, - ), - ); - }); - } - - QueryBuilder fileSizeLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileSize', - value: value, - ), - ); - }); - } - - QueryBuilder fileSizeBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileSize', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'id'), - ); - }); - } - - QueryBuilder idIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'id'), - ); - }); - } - - QueryBuilder idEqualTo(Id? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id? lower, - Id? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder isoIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'iso'), - ); - }); - } - - QueryBuilder isoIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'iso'), - ); - }); - } - - QueryBuilder isoEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'iso', value: value), - ); - }); - } - - QueryBuilder isoGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'iso', - value: value, - ), - ); - }); - } - - QueryBuilder isoLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'iso', - value: value, - ), - ); - }); - } - - QueryBuilder isoBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'iso', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder latIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'lat'), - ); - }); - } - - QueryBuilder latIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'lat'), - ); - }); - } - - QueryBuilder latEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'lat', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder latGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lat', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder latLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lat', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder latBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lat', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder lensIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'lens'), - ); - }); - } - - QueryBuilder lensIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'lens'), - ); - }); - } - - QueryBuilder lensEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lens', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'lens', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'lens', value: ''), - ); - }); - } - - QueryBuilder lensIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'lens', value: ''), - ); - }); - } - - QueryBuilder longIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'long'), - ); - }); - } - - QueryBuilder longIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'long'), - ); - }); - } - - QueryBuilder longEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'long', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder longGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'long', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder longLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'long', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder longBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'long', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder makeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'make'), - ); - }); - } - - QueryBuilder makeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'make'), - ); - }); - } - - QueryBuilder makeEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'make', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'make', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'make', value: ''), - ); - }); - } - - QueryBuilder makeIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'make', value: ''), - ); - }); - } - - QueryBuilder mmIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'mm'), - ); - }); - } - - QueryBuilder mmIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'mm'), - ); - }); - } - - QueryBuilder mmEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'mm', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder mmGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'mm', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder mmLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'mm', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder mmBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'mm', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder modelIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'model'), - ); - }); - } - - QueryBuilder modelIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'model'), - ); - }); - } - - QueryBuilder modelEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'model', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'model', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'model', value: ''), - ); - }); - } - - QueryBuilder modelIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'model', value: ''), - ); - }); - } - - QueryBuilder orientationIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'orientation'), - ); - }); - } - - QueryBuilder - orientationIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'orientation'), - ); - }); - } - - QueryBuilder orientationEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - orientationGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'orientation', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'orientation', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'orientation', value: ''), - ); - }); - } - - QueryBuilder - orientationIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'orientation', value: ''), - ); - }); - } - - QueryBuilder stateIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'state'), - ); - }); - } - - QueryBuilder stateIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'state'), - ); - }); - } - - QueryBuilder stateEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'state', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'state', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'state', value: ''), - ); - }); - } - - QueryBuilder stateIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'state', value: ''), - ); - }); - } - - QueryBuilder timeZoneIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'timeZone'), - ); - }); - } - - QueryBuilder timeZoneIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'timeZone'), - ); - }); - } - - QueryBuilder timeZoneEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'timeZone', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'timeZone', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'timeZone', value: ''), - ); - }); - } - - QueryBuilder timeZoneIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'timeZone', value: ''), - ); - }); - } -} - -extension ExifInfoQueryObject - on QueryBuilder {} - -extension ExifInfoQueryLinks - on QueryBuilder {} - -extension ExifInfoQuerySortBy on QueryBuilder { - QueryBuilder sortByCity() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.asc); - }); - } - - QueryBuilder sortByCityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.desc); - }); - } - - QueryBuilder sortByCountry() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.asc); - }); - } - - QueryBuilder sortByCountryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.desc); - }); - } - - QueryBuilder sortByDateTimeOriginal() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.asc); - }); - } - - QueryBuilder sortByDateTimeOriginalDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.desc); - }); - } - - QueryBuilder sortByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder sortByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder sortByExposureSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.asc); - }); - } - - QueryBuilder sortByExposureSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.desc); - }); - } - - QueryBuilder sortByF() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.asc); - }); - } - - QueryBuilder sortByFDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.desc); - }); - } - - QueryBuilder sortByFileSize() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.asc); - }); - } - - QueryBuilder sortByFileSizeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.desc); - }); - } - - QueryBuilder sortByIso() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.asc); - }); - } - - QueryBuilder sortByIsoDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.desc); - }); - } - - QueryBuilder sortByLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.asc); - }); - } - - QueryBuilder sortByLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.desc); - }); - } - - QueryBuilder sortByLens() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.asc); - }); - } - - QueryBuilder sortByLensDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.desc); - }); - } - - QueryBuilder sortByLong() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.asc); - }); - } - - QueryBuilder sortByLongDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.desc); - }); - } - - QueryBuilder sortByMake() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.asc); - }); - } - - QueryBuilder sortByMakeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.desc); - }); - } - - QueryBuilder sortByMm() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.asc); - }); - } - - QueryBuilder sortByMmDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.desc); - }); - } - - QueryBuilder sortByModel() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.asc); - }); - } - - QueryBuilder sortByModelDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.desc); - }); - } - - QueryBuilder sortByOrientation() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.asc); - }); - } - - QueryBuilder sortByOrientationDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.desc); - }); - } - - QueryBuilder sortByState() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.asc); - }); - } - - QueryBuilder sortByStateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.desc); - }); - } - - QueryBuilder sortByTimeZone() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.asc); - }); - } - - QueryBuilder sortByTimeZoneDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.desc); - }); - } -} - -extension ExifInfoQuerySortThenBy - on QueryBuilder { - QueryBuilder thenByCity() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.asc); - }); - } - - QueryBuilder thenByCityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.desc); - }); - } - - QueryBuilder thenByCountry() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.asc); - }); - } - - QueryBuilder thenByCountryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.desc); - }); - } - - QueryBuilder thenByDateTimeOriginal() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.asc); - }); - } - - QueryBuilder thenByDateTimeOriginalDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.desc); - }); - } - - QueryBuilder thenByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder thenByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder thenByExposureSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.asc); - }); - } - - QueryBuilder thenByExposureSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.desc); - }); - } - - QueryBuilder thenByF() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.asc); - }); - } - - QueryBuilder thenByFDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.desc); - }); - } - - QueryBuilder thenByFileSize() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.asc); - }); - } - - QueryBuilder thenByFileSizeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIso() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.asc); - }); - } - - QueryBuilder thenByIsoDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.desc); - }); - } - - QueryBuilder thenByLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.asc); - }); - } - - QueryBuilder thenByLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.desc); - }); - } - - QueryBuilder thenByLens() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.asc); - }); - } - - QueryBuilder thenByLensDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.desc); - }); - } - - QueryBuilder thenByLong() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.asc); - }); - } - - QueryBuilder thenByLongDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.desc); - }); - } - - QueryBuilder thenByMake() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.asc); - }); - } - - QueryBuilder thenByMakeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.desc); - }); - } - - QueryBuilder thenByMm() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.asc); - }); - } - - QueryBuilder thenByMmDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.desc); - }); - } - - QueryBuilder thenByModel() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.asc); - }); - } - - QueryBuilder thenByModelDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.desc); - }); - } - - QueryBuilder thenByOrientation() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.asc); - }); - } - - QueryBuilder thenByOrientationDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.desc); - }); - } - - QueryBuilder thenByState() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.asc); - }); - } - - QueryBuilder thenByStateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.desc); - }); - } - - QueryBuilder thenByTimeZone() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.asc); - }); - } - - QueryBuilder thenByTimeZoneDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.desc); - }); - } -} - -extension ExifInfoQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByCity({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'city', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByCountry({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'country', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByDateTimeOriginal() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'dateTimeOriginal'); - }); - } - - QueryBuilder distinctByDescription({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'description', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByExposureSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'exposureSeconds'); - }); - } - - QueryBuilder distinctByF() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'f'); - }); - } - - QueryBuilder distinctByFileSize() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileSize'); - }); - } - - QueryBuilder distinctByIso() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'iso'); - }); - } - - QueryBuilder distinctByLat() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lat'); - }); - } - - QueryBuilder distinctByLens({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lens', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByLong() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'long'); - }); - } - - QueryBuilder distinctByMake({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'make', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByMm() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'mm'); - }); - } - - QueryBuilder distinctByModel({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'model', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByOrientation({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'orientation', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByState({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'state', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByTimeZone({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'timeZone', caseSensitive: caseSensitive); - }); - } -} - -extension ExifInfoQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder cityProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'city'); - }); - } - - QueryBuilder countryProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'country'); - }); - } - - QueryBuilder - dateTimeOriginalProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'dateTimeOriginal'); - }); - } - - QueryBuilder descriptionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'description'); - }); - } - - QueryBuilder exposureSecondsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'exposureSeconds'); - }); - } - - QueryBuilder fProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'f'); - }); - } - - QueryBuilder fileSizeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileSize'); - }); - } - - QueryBuilder isoProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'iso'); - }); - } - - QueryBuilder latProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lat'); - }); - } - - QueryBuilder lensProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lens'); - }); - } - - QueryBuilder longProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'long'); - }); - } - - QueryBuilder makeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'make'); - }); - } - - QueryBuilder mmProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'mm'); - }); - } - - QueryBuilder modelProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'model'); - }); - } - - QueryBuilder orientationProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'orientation'); - }); - } - - QueryBuilder stateProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'state'); - }); - } - - QueryBuilder timeZoneProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'timeZone'); - }); - } -} diff --git a/mobile/lib/infrastructure/entities/store.entity.dart b/mobile/lib/infrastructure/entities/store.entity.dart index d4b3eec84f..2de8eb713e 100644 --- a/mobile/lib/infrastructure/entities/store.entity.dart +++ b/mobile/lib/infrastructure/entities/store.entity.dart @@ -1,18 +1,5 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; -import 'package:isar/isar.dart'; - -part 'store.entity.g.dart'; - -/// Internal class for `Store`, do not use elsewhere. -@Collection(inheritance: false) -class StoreValue { - final Id id; - final int? intValue; - final String? strValue; - - const StoreValue(this.id, {this.intValue, this.strValue}); -} class StoreEntity extends Table with DriftDefaultsMixin { IntColumn get id => integer()(); diff --git a/mobile/lib/infrastructure/entities/store.entity.g.dart b/mobile/lib/infrastructure/entities/store.entity.g.dart deleted file mode 100644 index 626c3084fe..0000000000 --- a/mobile/lib/infrastructure/entities/store.entity.g.dart +++ /dev/null @@ -1,596 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'store.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetStoreValueCollection on Isar { - IsarCollection get storeValues => this.collection(); -} - -const StoreValueSchema = CollectionSchema( - name: r'StoreValue', - id: 902899285492123510, - properties: { - r'intValue': PropertySchema(id: 0, name: r'intValue', type: IsarType.long), - r'strValue': PropertySchema( - id: 1, - name: r'strValue', - type: IsarType.string, - ), - }, - - estimateSize: _storeValueEstimateSize, - serialize: _storeValueSerialize, - deserialize: _storeValueDeserialize, - deserializeProp: _storeValueDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _storeValueGetId, - getLinks: _storeValueGetLinks, - attach: _storeValueAttach, - version: '3.3.0-dev.3', -); - -int _storeValueEstimateSize( - StoreValue object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.strValue; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _storeValueSerialize( - StoreValue object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeLong(offsets[0], object.intValue); - writer.writeString(offsets[1], object.strValue); -} - -StoreValue _storeValueDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = StoreValue( - id, - intValue: reader.readLongOrNull(offsets[0]), - strValue: reader.readStringOrNull(offsets[1]), - ); - return object; -} - -P _storeValueDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readLongOrNull(offset)) as P; - case 1: - return (reader.readStringOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _storeValueGetId(StoreValue object) { - return object.id; -} - -List> _storeValueGetLinks(StoreValue object) { - return []; -} - -void _storeValueAttach(IsarCollection col, Id id, StoreValue object) {} - -extension StoreValueQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension StoreValueQueryWhere - on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension StoreValueQueryFilter - on QueryBuilder { - QueryBuilder idEqualTo( - Id value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder intValueIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'intValue'), - ); - }); - } - - QueryBuilder - intValueIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'intValue'), - ); - }); - } - - QueryBuilder intValueEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'intValue', value: value), - ); - }); - } - - QueryBuilder - intValueGreaterThan(int? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'intValue', - value: value, - ), - ); - }); - } - - QueryBuilder intValueLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'intValue', - value: value, - ), - ); - }); - } - - QueryBuilder intValueBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'intValue', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder strValueIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'strValue'), - ); - }); - } - - QueryBuilder - strValueIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'strValue'), - ); - }); - } - - QueryBuilder strValueEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - strValueGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'strValue', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - strValueStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'strValue', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - strValueIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'strValue', value: ''), - ); - }); - } - - QueryBuilder - strValueIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'strValue', value: ''), - ); - }); - } -} - -extension StoreValueQueryObject - on QueryBuilder {} - -extension StoreValueQueryLinks - on QueryBuilder {} - -extension StoreValueQuerySortBy - on QueryBuilder { - QueryBuilder sortByIntValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.asc); - }); - } - - QueryBuilder sortByIntValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.desc); - }); - } - - QueryBuilder sortByStrValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.asc); - }); - } - - QueryBuilder sortByStrValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.desc); - }); - } -} - -extension StoreValueQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIntValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.asc); - }); - } - - QueryBuilder thenByIntValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.desc); - }); - } - - QueryBuilder thenByStrValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.asc); - }); - } - - QueryBuilder thenByStrValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.desc); - }); - } -} - -extension StoreValueQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByIntValue() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'intValue'); - }); - } - - QueryBuilder distinctByStrValue({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'strValue', caseSensitive: caseSensitive); - }); - } -} - -extension StoreValueQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder intValueProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'intValue'); - }); - } - - QueryBuilder strValueProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'strValue'); - }); - } -} diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index 667a9d6a59..8d4371672c 100644 --- a/mobile/lib/infrastructure/entities/user.entity.dart +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -1,79 +1,6 @@ import 'package:drift/drift.dart' hide Index; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'user.entity.g.dart'; - -@Collection(inheritance: false) -class User { - Id get isarId => fastHash(id); - @Index(unique: true, replace: false, type: IndexType.hash) - final String id; - final DateTime updatedAt; - final String email; - final String name; - final bool isPartnerSharedBy; - final bool isPartnerSharedWith; - final bool isAdmin; - final String profileImagePath; - @Enumerated(EnumType.ordinal) - final AvatarColor avatarColor; - final bool memoryEnabled; - final bool inTimeline; - final int quotaUsageInBytes; - final int quotaSizeInBytes; - - const User({ - required this.id, - required this.updatedAt, - required this.email, - required this.name, - required this.isAdmin, - this.isPartnerSharedBy = false, - this.isPartnerSharedWith = false, - this.profileImagePath = '', - this.avatarColor = AvatarColor.primary, - this.memoryEnabled = true, - this.inTimeline = false, - this.quotaUsageInBytes = 0, - this.quotaSizeInBytes = 0, - }); - - static User fromDto(UserDto dto) => User( - id: dto.id, - updatedAt: dto.updatedAt ?? DateTime(2025), - email: dto.email, - name: dto.name, - isAdmin: dto.isAdmin, - isPartnerSharedBy: dto.isPartnerSharedBy, - isPartnerSharedWith: dto.isPartnerSharedWith, - profileImagePath: dto.hasProfileImage ? "HAS_PROFILE_IMAGE" : "", - avatarColor: dto.avatarColor, - memoryEnabled: dto.memoryEnabled, - inTimeline: dto.inTimeline, - quotaUsageInBytes: dto.quotaUsageInBytes, - quotaSizeInBytes: dto.quotaSizeInBytes, - ); - - UserDto toDto() => UserDto( - id: id, - email: email, - name: name, - isAdmin: isAdmin, - updatedAt: updatedAt, - avatarColor: avatarColor, - memoryEnabled: memoryEnabled, - inTimeline: inTimeline, - isPartnerSharedBy: isPartnerSharedBy, - isPartnerSharedWith: isPartnerSharedWith, - hasProfileImage: profileImagePath.isNotEmpty, - profileChangedAt: updatedAt, - quotaUsageInBytes: quotaUsageInBytes, - quotaSizeInBytes: quotaSizeInBytes, - ); -} class UserEntity extends Table with DriftDefaultsMixin { const UserEntity(); diff --git a/mobile/lib/infrastructure/entities/user.entity.g.dart b/mobile/lib/infrastructure/entities/user.entity.g.dart deleted file mode 100644 index 7e0af41b77..0000000000 --- a/mobile/lib/infrastructure/entities/user.entity.g.dart +++ /dev/null @@ -1,1854 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'user.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetUserCollection on Isar { - IsarCollection get users => this.collection(); -} - -const UserSchema = CollectionSchema( - name: r'User', - id: -7838171048429979076, - properties: { - r'avatarColor': PropertySchema( - id: 0, - name: r'avatarColor', - type: IsarType.byte, - enumMap: _UseravatarColorEnumValueMap, - ), - r'email': PropertySchema(id: 1, name: r'email', type: IsarType.string), - r'id': PropertySchema(id: 2, name: r'id', type: IsarType.string), - r'inTimeline': PropertySchema( - id: 3, - name: r'inTimeline', - type: IsarType.bool, - ), - r'isAdmin': PropertySchema(id: 4, name: r'isAdmin', type: IsarType.bool), - r'isPartnerSharedBy': PropertySchema( - id: 5, - name: r'isPartnerSharedBy', - type: IsarType.bool, - ), - r'isPartnerSharedWith': PropertySchema( - id: 6, - name: r'isPartnerSharedWith', - type: IsarType.bool, - ), - r'memoryEnabled': PropertySchema( - id: 7, - name: r'memoryEnabled', - type: IsarType.bool, - ), - r'name': PropertySchema(id: 8, name: r'name', type: IsarType.string), - r'profileImagePath': PropertySchema( - id: 9, - name: r'profileImagePath', - type: IsarType.string, - ), - r'quotaSizeInBytes': PropertySchema( - id: 10, - name: r'quotaSizeInBytes', - type: IsarType.long, - ), - r'quotaUsageInBytes': PropertySchema( - id: 11, - name: r'quotaUsageInBytes', - type: IsarType.long, - ), - r'updatedAt': PropertySchema( - id: 12, - name: r'updatedAt', - type: IsarType.dateTime, - ), - }, - - estimateSize: _userEstimateSize, - serialize: _userSerialize, - deserialize: _userDeserialize, - deserializeProp: _userDeserializeProp, - idName: r'isarId', - indexes: { - r'id': IndexSchema( - id: -3268401673993471357, - name: r'id', - unique: true, - replace: false, - properties: [ - IndexPropertySchema( - name: r'id', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _userGetId, - getLinks: _userGetLinks, - attach: _userAttach, - version: '3.3.0-dev.3', -); - -int _userEstimateSize( - User object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.email.length * 3; - bytesCount += 3 + object.id.length * 3; - bytesCount += 3 + object.name.length * 3; - bytesCount += 3 + object.profileImagePath.length * 3; - return bytesCount; -} - -void _userSerialize( - User object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByte(offsets[0], object.avatarColor.index); - writer.writeString(offsets[1], object.email); - writer.writeString(offsets[2], object.id); - writer.writeBool(offsets[3], object.inTimeline); - writer.writeBool(offsets[4], object.isAdmin); - writer.writeBool(offsets[5], object.isPartnerSharedBy); - writer.writeBool(offsets[6], object.isPartnerSharedWith); - writer.writeBool(offsets[7], object.memoryEnabled); - writer.writeString(offsets[8], object.name); - writer.writeString(offsets[9], object.profileImagePath); - writer.writeLong(offsets[10], object.quotaSizeInBytes); - writer.writeLong(offsets[11], object.quotaUsageInBytes); - writer.writeDateTime(offsets[12], object.updatedAt); -} - -User _userDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = User( - avatarColor: - _UseravatarColorValueEnumMap[reader.readByteOrNull(offsets[0])] ?? - AvatarColor.primary, - email: reader.readString(offsets[1]), - id: reader.readString(offsets[2]), - inTimeline: reader.readBoolOrNull(offsets[3]) ?? false, - isAdmin: reader.readBool(offsets[4]), - isPartnerSharedBy: reader.readBoolOrNull(offsets[5]) ?? false, - isPartnerSharedWith: reader.readBoolOrNull(offsets[6]) ?? false, - memoryEnabled: reader.readBoolOrNull(offsets[7]) ?? true, - name: reader.readString(offsets[8]), - profileImagePath: reader.readStringOrNull(offsets[9]) ?? '', - quotaSizeInBytes: reader.readLongOrNull(offsets[10]) ?? 0, - quotaUsageInBytes: reader.readLongOrNull(offsets[11]) ?? 0, - updatedAt: reader.readDateTime(offsets[12]), - ); - return object; -} - -P _userDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (_UseravatarColorValueEnumMap[reader.readByteOrNull(offset)] ?? - AvatarColor.primary) - as P; - case 1: - return (reader.readString(offset)) as P; - case 2: - return (reader.readString(offset)) as P; - case 3: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 4: - return (reader.readBool(offset)) as P; - case 5: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 6: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 7: - return (reader.readBoolOrNull(offset) ?? true) as P; - case 8: - return (reader.readString(offset)) as P; - case 9: - return (reader.readStringOrNull(offset) ?? '') as P; - case 10: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 11: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 12: - return (reader.readDateTime(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _UseravatarColorEnumValueMap = { - 'primary': 0, - 'pink': 1, - 'red': 2, - 'yellow': 3, - 'blue': 4, - 'green': 5, - 'purple': 6, - 'orange': 7, - 'gray': 8, - 'amber': 9, -}; -const _UseravatarColorValueEnumMap = { - 0: AvatarColor.primary, - 1: AvatarColor.pink, - 2: AvatarColor.red, - 3: AvatarColor.yellow, - 4: AvatarColor.blue, - 5: AvatarColor.green, - 6: AvatarColor.purple, - 7: AvatarColor.orange, - 8: AvatarColor.gray, - 9: AvatarColor.amber, -}; - -Id _userGetId(User object) { - return object.isarId; -} - -List> _userGetLinks(User object) { - return []; -} - -void _userAttach(IsarCollection col, Id id, User object) {} - -extension UserByIndex on IsarCollection { - Future getById(String id) { - return getByIndex(r'id', [id]); - } - - User? getByIdSync(String id) { - return getByIndexSync(r'id', [id]); - } - - Future deleteById(String id) { - return deleteByIndex(r'id', [id]); - } - - bool deleteByIdSync(String id) { - return deleteByIndexSync(r'id', [id]); - } - - Future> getAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndex(r'id', values); - } - - List getAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'id', values); - } - - Future deleteAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'id', values); - } - - int deleteAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'id', values); - } - - Future putById(User object) { - return putByIndex(r'id', object); - } - - Id putByIdSync(User object, {bool saveLinks = true}) { - return putByIndexSync(r'id', object, saveLinks: saveLinks); - } - - Future> putAllById(List objects) { - return putAllByIndex(r'id', objects); - } - - List putAllByIdSync(List objects, {bool saveLinks = true}) { - return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); - } -} - -extension UserQueryWhereSort on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension UserQueryWhere on QueryBuilder { - QueryBuilder isarIdEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder isarIdGreaterThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'id', value: [id]), - ); - }); - } - - QueryBuilder idNotEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ); - } - }); - } -} - -extension UserQueryFilter on QueryBuilder { - QueryBuilder avatarColorEqualTo( - AvatarColor value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'avatarColor', value: value), - ); - }); - } - - QueryBuilder avatarColorGreaterThan( - AvatarColor value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'avatarColor', - value: value, - ), - ); - }); - } - - QueryBuilder avatarColorLessThan( - AvatarColor value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'avatarColor', - value: value, - ), - ); - }); - } - - QueryBuilder avatarColorBetween( - AvatarColor lower, - AvatarColor upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'avatarColor', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder emailEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'email', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'email', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'email', value: ''), - ); - }); - } - - QueryBuilder emailIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'email', value: ''), - ); - }); - } - - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder inTimelineEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'inTimeline', value: value), - ); - }); - } - - QueryBuilder isAdminEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isAdmin', value: value), - ); - }); - } - - QueryBuilder isPartnerSharedByEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isPartnerSharedBy', value: value), - ); - }); - } - - QueryBuilder isPartnerSharedWithEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isPartnerSharedWith', value: value), - ); - }); - } - - QueryBuilder isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder isarIdGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder memoryEnabledEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'memoryEnabled', value: value), - ); - }); - } - - QueryBuilder nameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'name', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'name', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'name', value: ''), - ); - }); - } - - QueryBuilder nameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'name', value: ''), - ); - }); - } - - QueryBuilder profileImagePathEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'profileImagePath', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'profileImagePath', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'profileImagePath', value: ''), - ); - }); - } - - QueryBuilder profileImagePathIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'profileImagePath', value: ''), - ); - }); - } - - QueryBuilder quotaSizeInBytesEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'quotaSizeInBytes', value: value), - ); - }); - } - - QueryBuilder quotaSizeInBytesGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'quotaSizeInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaSizeInBytesLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'quotaSizeInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaSizeInBytesBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'quotaSizeInBytes', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder quotaUsageInBytesEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'quotaUsageInBytes', value: value), - ); - }); - } - - QueryBuilder quotaUsageInBytesGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'quotaUsageInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaUsageInBytesLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'quotaUsageInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaUsageInBytesBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'quotaUsageInBytes', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder updatedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'updatedAt', value: value), - ); - }); - } - - QueryBuilder updatedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'updatedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension UserQueryObject on QueryBuilder {} - -extension UserQueryLinks on QueryBuilder {} - -extension UserQuerySortBy on QueryBuilder { - QueryBuilder sortByAvatarColor() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.asc); - }); - } - - QueryBuilder sortByAvatarColorDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.desc); - }); - } - - QueryBuilder sortByEmail() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.asc); - }); - } - - QueryBuilder sortByEmailDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.desc); - }); - } - - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder sortByInTimeline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.asc); - }); - } - - QueryBuilder sortByInTimelineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.desc); - }); - } - - QueryBuilder sortByIsAdmin() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.asc); - }); - } - - QueryBuilder sortByIsAdminDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.desc); - }); - } - - QueryBuilder sortByIsPartnerSharedBy() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.asc); - }); - } - - QueryBuilder sortByIsPartnerSharedByDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.desc); - }); - } - - QueryBuilder sortByIsPartnerSharedWith() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.asc); - }); - } - - QueryBuilder sortByIsPartnerSharedWithDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.desc); - }); - } - - QueryBuilder sortByMemoryEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.asc); - }); - } - - QueryBuilder sortByMemoryEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.desc); - }); - } - - QueryBuilder sortByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder sortByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder sortByProfileImagePath() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.asc); - }); - } - - QueryBuilder sortByProfileImagePathDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.desc); - }); - } - - QueryBuilder sortByQuotaSizeInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.asc); - }); - } - - QueryBuilder sortByQuotaSizeInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.desc); - }); - } - - QueryBuilder sortByQuotaUsageInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.asc); - }); - } - - QueryBuilder sortByQuotaUsageInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.desc); - }); - } - - QueryBuilder sortByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder sortByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } -} - -extension UserQuerySortThenBy on QueryBuilder { - QueryBuilder thenByAvatarColor() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.asc); - }); - } - - QueryBuilder thenByAvatarColorDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.desc); - }); - } - - QueryBuilder thenByEmail() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.asc); - }); - } - - QueryBuilder thenByEmailDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByInTimeline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.asc); - }); - } - - QueryBuilder thenByInTimelineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.desc); - }); - } - - QueryBuilder thenByIsAdmin() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.asc); - }); - } - - QueryBuilder thenByIsAdminDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.desc); - }); - } - - QueryBuilder thenByIsPartnerSharedBy() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.asc); - }); - } - - QueryBuilder thenByIsPartnerSharedByDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.desc); - }); - } - - QueryBuilder thenByIsPartnerSharedWith() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.asc); - }); - } - - QueryBuilder thenByIsPartnerSharedWithDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - - QueryBuilder thenByMemoryEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.asc); - }); - } - - QueryBuilder thenByMemoryEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.desc); - }); - } - - QueryBuilder thenByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder thenByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder thenByProfileImagePath() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.asc); - }); - } - - QueryBuilder thenByProfileImagePathDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.desc); - }); - } - - QueryBuilder thenByQuotaSizeInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.asc); - }); - } - - QueryBuilder thenByQuotaSizeInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.desc); - }); - } - - QueryBuilder thenByQuotaUsageInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.asc); - }); - } - - QueryBuilder thenByQuotaUsageInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.desc); - }); - } - - QueryBuilder thenByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder thenByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } -} - -extension UserQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByAvatarColor() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'avatarColor'); - }); - } - - QueryBuilder distinctByEmail({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'email', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByInTimeline() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'inTimeline'); - }); - } - - QueryBuilder distinctByIsAdmin() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isAdmin'); - }); - } - - QueryBuilder distinctByIsPartnerSharedBy() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isPartnerSharedBy'); - }); - } - - QueryBuilder distinctByIsPartnerSharedWith() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isPartnerSharedWith'); - }); - } - - QueryBuilder distinctByMemoryEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'memoryEnabled'); - }); - } - - QueryBuilder distinctByName({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'name', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByProfileImagePath({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'profileImagePath', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByQuotaSizeInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'quotaSizeInBytes'); - }); - } - - QueryBuilder distinctByQuotaUsageInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'quotaUsageInBytes'); - }); - } - - QueryBuilder distinctByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'updatedAt'); - }); - } -} - -extension UserQueryProperty on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder avatarColorProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'avatarColor'); - }); - } - - QueryBuilder emailProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'email'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder inTimelineProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'inTimeline'); - }); - } - - QueryBuilder isAdminProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isAdmin'); - }); - } - - QueryBuilder isPartnerSharedByProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isPartnerSharedBy'); - }); - } - - QueryBuilder isPartnerSharedWithProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isPartnerSharedWith'); - }); - } - - QueryBuilder memoryEnabledProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'memoryEnabled'); - }); - } - - QueryBuilder nameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'name'); - }); - } - - QueryBuilder profileImagePathProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'profileImagePath'); - }); - } - - QueryBuilder quotaSizeInBytesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'quotaSizeInBytes'); - }); - } - - QueryBuilder quotaUsageInBytesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'quotaUsageInBytes'); - }); - } - - QueryBuilder updatedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'updatedAt'); - }); - } -} diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index d41891e2ea..eca8810b91 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:drift/drift.dart'; 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_edit.entity.dart'; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart'; import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart'; @@ -27,22 +26,6 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart'; -import 'package:isar/isar.dart' hide Index; - -// #zoneTxn is the symbol used by Isar to mark a transaction within the current zone -// ref: isar/isar_common.dart -const Symbol _kzoneTxn = #zoneTxn; - -class IsarDatabaseRepository implements IDatabaseRepository { - final Isar _db; - const IsarDatabaseRepository(Isar db) : _db = db; - - // Isar do not support nested transactions. This is a workaround to prevent us from making nested transactions - // Reuse the current transaction if it is already active, else start a new transaction - @override - Future transaction(Future Function() callback) => - Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback(); -} @DriftDatabase( tables: [ @@ -70,7 +53,7 @@ class IsarDatabaseRepository implements IDatabaseRepository { ], include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'}, ) -class Drift extends $Drift implements IDatabaseRepository { +class Drift extends $Drift { Drift([QueryExecutor? executor]) : super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true))); @@ -261,10 +244,9 @@ class Drift extends $Drift implements IDatabaseRepository { ); } -class DriftDatabaseRepository implements IDatabaseRepository { +class DriftDatabaseRepository { final Drift _db; const DriftDatabaseRepository(this._db); - @override Future transaction(Future Function() callback) => _db.transaction(callback); } diff --git a/mobile/lib/infrastructure/repositories/device_asset.repository.dart b/mobile/lib/infrastructure/repositories/device_asset.repository.dart deleted file mode 100644 index 73ee148ab3..0000000000 --- a/mobile/lib/infrastructure/repositories/device_asset.repository.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; - -class IsarDeviceAssetRepository extends IsarDatabaseRepository { - final Isar _db; - - const IsarDeviceAssetRepository(this._db) : super(_db); - - Future deleteIds(List ids) { - return transaction(() async { - await _db.deviceAssetEntitys.deleteAllByAssetId(ids.toList()); - }); - } - - Future> getByIds(List localIds) { - return _db.deviceAssetEntitys - .where() - .anyOf(localIds, (query, id) => query.assetIdEqualTo(id)) - .findAll() - .then((value) => value.map((e) => e.toModel()).toList()); - } - - Future updateAll(List assetHash) { - return transaction(() async { - await _db.deviceAssetEntitys.putAll(assetHash.map(DeviceAssetEntity.fromDto).toList()); - return true; - }); - } -} diff --git a/mobile/lib/infrastructure/repositories/exif.repository.dart b/mobile/lib/infrastructure/repositories/exif.repository.dart deleted file mode 100644 index 0ede30680e..0000000000 --- a/mobile/lib/infrastructure/repositories/exif.repository.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; - -class IsarExifRepository extends IsarDatabaseRepository { - final Isar _db; - - const IsarExifRepository(this._db) : super(_db); - - Future delete(int assetId) async { - await transaction(() async { - await _db.exifInfos.delete(assetId); - }); - } - - Future deleteAll() async { - await transaction(() async { - await _db.exifInfos.clear(); - }); - } - - Future get(int assetId) async { - return (await _db.exifInfos.get(assetId))?.toDto(); - } - - Future update(ExifInfo exifInfo) { - return transaction(() async { - await _db.exifInfos.put(entity.ExifInfo.fromDto(exifInfo)); - return exifInfo; - }); - } - - Future> updateAll(List exifInfos) { - return transaction(() async { - await _db.exifInfos.putAll(exifInfos.map(entity.ExifInfo.fromDto).toList()); - return exifInfos; - }); - } -} diff --git a/mobile/lib/infrastructure/repositories/logger_db.repository.dart b/mobile/lib/infrastructure/repositories/logger_db.repository.dart index e494782fa6..d11174356d 100644 --- a/mobile/lib/infrastructure/repositories/logger_db.repository.dart +++ b/mobile/lib/infrastructure/repositories/logger_db.repository.dart @@ -1,11 +1,10 @@ import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.drift.dart'; @DriftDatabase(tables: [LogMessageEntity]) -class DriftLogger extends $DriftLogger implements IDatabaseRepository { +class DriftLogger extends $DriftLogger { DriftLogger([QueryExecutor? executor]) : super( executor ?? driftDatabase(name: 'immich_logs', native: const DriftNativeOptions(shareAcrossIsolates: true)), diff --git a/mobile/lib/infrastructure/repositories/network.repository.dart b/mobile/lib/infrastructure/repositories/network.repository.dart index bb5796e220..75f7850168 100644 --- a/mobile/lib/infrastructure/repositories/network.repository.dart +++ b/mobile/lib/infrastructure/repositories/network.repository.dart @@ -22,7 +22,14 @@ class NetworkRepository { final session = URLSession.fromRawPointer(clientPointer.cast()); _client = CupertinoClient.fromSharedSession(session); } else { - _client = OkHttpClient.fromJniGlobalRef(clientPointer); + _client = OkHttpClient.fromJniGlobalRef( + clientPointer, + configuration: const OkHttpClientConfiguration( + connectTimeout: Duration(seconds: 30), + readTimeout: Duration(seconds: 60), + writeTimeout: Duration(seconds: 60), + ), + ); } } diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index df4172df99..6d19d17931 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -1,8 +1,10 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/stack.model.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' hide ExifInfo; +import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; @@ -264,4 +266,11 @@ class RemoteAssetRepository extends DriftDatabaseRepository { Future getCount() { return _db.managers.remoteAssetEntity.count(); } + + Future> getAssetEdits(String assetId) { + final query = _db.assetEditEntity.select() + ..where((row) => row.assetId.equals(assetId) & row.action.equals(AssetEditAction.other.index).not()) + ..orderBy([(row) => OrderingTerm.asc(row.sequence)]); + return query.map((row) => row.toDto()!).get(); + } } diff --git a/mobile/lib/infrastructure/repositories/store.repository.dart b/mobile/lib/infrastructure/repositories/store.repository.dart index d4e34a02f5..9680aa0425 100644 --- a/mobile/lib/infrastructure/repositories/store.repository.dart +++ b/mobile/lib/infrastructure/repositories/store.repository.dart @@ -1,150 +1,42 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:isar/isar.dart'; -// Temporary interface until Isar is removed to make the service work with both Isar and Sqlite -abstract class IStoreRepository { - Future deleteAll(); - Stream>> watchAll(); - Future delete(StoreKey key); - Future upsert(StoreKey key, T value); - Future tryGet(StoreKey key); - Stream watch(StoreKey key); - Future>> getAll(); -} - -class IsarStoreRepository extends IsarDatabaseRepository implements IStoreRepository { - final Isar _db; - final validStoreKeys = StoreKey.values.map((e) => e.id).toSet(); - - IsarStoreRepository(super.db) : _db = db; - - @override - Future deleteAll() async { - return await transaction(() async { - await _db.storeValues.clear(); - return true; - }); - } - - @override - Stream>> watchAll() { - return _db.storeValues - .filter() - .anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)) - .watch(fireImmediately: true) - .asyncMap((entities) => Future.wait(entities.map((entity) => _toUpdateEvent(entity)))); - } - - @override - Future delete(StoreKey key) async { - return await transaction(() async => await _db.storeValues.delete(key.id)); - } - - @override - Future upsert(StoreKey key, T value) async { - return await transaction(() async { - await _db.storeValues.put(await _fromValue(key, value)); - return true; - }); - } - - @override - Future tryGet(StoreKey key) async { - final entity = (await _db.storeValues.get(key.id)); - if (entity == null) { - return null; - } - return await _toValue(key, entity); - } - - @override - Stream watch(StoreKey key) async* { - yield* _db.storeValues - .watchObject(key.id, fireImmediately: true) - .asyncMap((e) async => e == null ? null : await _toValue(key, e)); - } - - Future> _toUpdateEvent(StoreValue entity) async { - final key = StoreKey.values.firstWhere((e) => e.id == entity.id) as StoreKey; - final value = await _toValue(key, entity); - return StoreDto(key, value); - } - - Future _toValue(StoreKey key, StoreValue entity) async => - switch (key.type) { - const (int) => entity.intValue, - const (String) => entity.strValue, - const (bool) => entity.intValue == 1, - const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!), - const (UserDto) => - entity.strValue == null ? null : await IsarUserRepository(_db).getByUserId(entity.strValue!), - _ => null, - } - as T?; - - Future _fromValue(StoreKey key, T value) async { - final (int? intValue, String? strValue) = switch (key.type) { - const (int) => (value as int, null), - 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 IsarUserRepository(_db).update(value as UserDto)).id), - _ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"), - }; - return StoreValue(key.id, intValue: intValue, strValue: strValue); - } - - @override - Future>> getAll() async { - final entities = await _db.storeValues.filter().anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)).findAll(); - return Future.wait(entities.map((e) => _toUpdateEvent(e)).toList()); - } -} - -class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepository { +class DriftStoreRepository extends DriftDatabaseRepository { final Drift _db; final validStoreKeys = StoreKey.values.map((e) => e.id).toSet(); DriftStoreRepository(super.db) : _db = db; - @override Future deleteAll() async { await _db.storeEntity.deleteAll(); return true; } - @override Future>> getAll() async { final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); return query.asyncMap((entity) => _toUpdateEvent(entity)).get(); } - @override Stream>> watchAll() { final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); return query.asyncMap((entity) => _toUpdateEvent(entity)).watch(); } - @override Future delete(StoreKey key) async { await _db.storeEntity.deleteWhere((entity) => entity.id.equals(key.id)); return; } - @override Future upsert(StoreKey key, T value) async { await _db.storeEntity.insertOnConflictUpdate(await _fromValue(key, value)); return true; } - @override Future tryGet(StoreKey key) async { final entity = await _db.managers.storeEntity.filter((entity) => entity.id.equals(key.id)).getSingleOrNull(); if (entity == null) { @@ -153,7 +45,6 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo return await _toValue(key, entity); } - @override Stream watch(StoreKey key) async* { final query = _db.storeEntity.select()..where((entity) => entity.id.equals(key.id)); diff --git a/mobile/lib/infrastructure/repositories/user.repository.dart b/mobile/lib/infrastructure/repositories/user.repository.dart index d4eb1ceed6..ce7cb124db 100644 --- a/mobile/lib/infrastructure/repositories/user.repository.dart +++ b/mobile/lib/infrastructure/repositories/user.repository.dart @@ -1,72 +1,9 @@ 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/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart'; -import 'package:isar/isar.dart'; - -class IsarUserRepository extends IsarDatabaseRepository { - final Isar _db; - const IsarUserRepository(super.db) : _db = db; - - Future delete(List ids) async { - await transaction(() async { - await _db.users.deleteAllById(ids); - }); - } - - Future deleteAll() async { - await transaction(() async { - await _db.users.clear(); - }); - } - - Future> getAll({SortUserBy? sortBy}) async { - return (await _db.users - .where() - .optional( - sortBy != null, - (query) => switch (sortBy!) { - SortUserBy.id => query.sortById(), - }, - ) - .findAll()) - .map((u) => u.toDto()) - .toList(); - } - - Future getByUserId(String id) async { - return (await _db.users.getById(id))?.toDto(); - } - - Future> getByUserIds(List ids) async { - return (await _db.users.getAllById(ids)).map((u) => u?.toDto()).toList(); - } - - Future insert(UserDto user) async { - await transaction(() async { - await _db.users.put(entity.User.fromDto(user)); - }); - return true; - } - - Future update(UserDto user) async { - await transaction(() async { - await _db.users.put(entity.User.fromDto(user)); - }); - return user; - } - - Future updateAll(List users) async { - await transaction(() async { - await _db.users.putAll(users.map(entity.User.fromDto).toList()); - }); - return true; - } -} class DriftAuthUserRepository extends DriftDatabaseRepository { final Drift _db; @@ -117,6 +54,7 @@ extension on AuthUserEntityData { id: id, email: email, name: name, + updatedAt: profileChangedAt, profileChangedAt: profileChangedAt, hasProfileImage: hasProfileImage, avatarColor: avatarColor, diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 7e7c709eeb..4a284b9bda 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -14,7 +14,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/domain/services/background_worker.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; @@ -24,7 +23,6 @@ import 'package:immich_mobile/pages/common/splash_screen.page.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; @@ -32,9 +30,7 @@ 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/background.service.dart'; import 'package:immich_mobile/services/deep_link.service.dart'; -import 'package:immich_mobile/services/local_notification.service.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; @@ -53,23 +49,13 @@ void main() async { ImmichWidgetsBinding(); unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock()); await EasyLocalization.ensureInitialized(); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); + final (drift, _) = await Bootstrap.initDomain(); await initApp(); // Warm-up isolate pool for worker manager await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5)); - await migrateDatabaseIfNeeded(isar, drift); + await migrateDatabaseIfNeeded(); - runApp( - ProviderScope( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - child: const MainWidget(), - ), - ); + runApp(ProviderScope(overrides: [driftProvider.overrideWith(driftOverride(drift))], child: const MainWidget())); } catch (error, stack) { runApp(BootstrapErrorWidget(error: error.toString(), stack: stack.toString())); } @@ -176,7 +162,6 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve } } SystemChrome.setSystemUIOverlayStyle(overlayStyle); - await ref.read(localNotificationService).setup(); } Future _deepLinkBuilder(PlatformDeepLink deepLink) async { @@ -215,20 +200,14 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve initApp().then((_) => dPrint(() => "App Init Completed")); WidgetsBinding.instance.addPostFrameCallback((_) { // needs to be delayed so that EasyLocalization is working - if (Store.isBetaTimelineEnabled) { - ref.read(backgroundServiceProvider).disableService(); - ref.read(backgroundWorkerFgServiceProvider).enable(); - if (Platform.isAndroid) { - ref - .read(backgroundWorkerFgServiceProvider) - .saveNotificationMessage( - StaticTranslations.instance.uploading_media, - StaticTranslations.instance.backup_background_service_default_notification, - ); - } - } else { - ref.read(backgroundWorkerFgServiceProvider).disable(); - ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); + ref.read(backgroundWorkerFgServiceProvider).enable(); + if (Platform.isAndroid) { + ref + .read(backgroundWorkerFgServiceProvider) + .saveNotificationMessage( + StaticTranslations.instance.uploading_media, + StaticTranslations.instance.backup_background_service_default_notification, + ); } }); diff --git a/mobile/lib/models/albums/album_add_asset_response.model.dart b/mobile/lib/models/albums/album_add_asset_response.model.dart deleted file mode 100644 index 38dd989af5..0000000000 --- a/mobile/lib/models/albums/album_add_asset_response.model.dart +++ /dev/null @@ -1,38 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; - -import 'package:collection/collection.dart'; - -class AlbumAddAssetsResponse { - List alreadyInAlbum; - int successfullyAdded; - - AlbumAddAssetsResponse({required this.alreadyInAlbum, required this.successfullyAdded}); - - AlbumAddAssetsResponse copyWith({List? alreadyInAlbum, int? successfullyAdded}) { - return AlbumAddAssetsResponse( - alreadyInAlbum: alreadyInAlbum ?? this.alreadyInAlbum, - successfullyAdded: successfullyAdded ?? this.successfullyAdded, - ); - } - - Map toMap() { - return {'alreadyInAlbum': alreadyInAlbum, 'successfullyAdded': successfullyAdded}; - } - - String toJson() => json.encode(toMap()); - - @override - String toString() => 'AddAssetsResponse(alreadyInAlbum: $alreadyInAlbum, successfullyAdded: $successfullyAdded)'; - - @override - bool operator ==(covariant AlbumAddAssetsResponse other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.alreadyInAlbum, alreadyInAlbum) && other.successfullyAdded == successfullyAdded; - } - - @override - int get hashCode => alreadyInAlbum.hashCode ^ successfullyAdded.hashCode; -} diff --git a/mobile/lib/models/albums/album_viewer_page_state.model.dart b/mobile/lib/models/albums/album_viewer_page_state.model.dart deleted file mode 100644 index 70427899ae..0000000000 --- a/mobile/lib/models/albums/album_viewer_page_state.model.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'dart:convert'; - -class AlbumViewerPageState { - final bool isEditAlbum; - final String editTitleText; - final String editDescriptionText; - - const AlbumViewerPageState({ - required this.isEditAlbum, - required this.editTitleText, - required this.editDescriptionText, - }); - - AlbumViewerPageState copyWith({bool? isEditAlbum, String? editTitleText, String? editDescriptionText}) { - return AlbumViewerPageState( - isEditAlbum: isEditAlbum ?? this.isEditAlbum, - editTitleText: editTitleText ?? this.editTitleText, - editDescriptionText: editDescriptionText ?? this.editDescriptionText, - ); - } - - Map toMap() { - final result = {}; - - result.addAll({'isEditAlbum': isEditAlbum}); - result.addAll({'editTitleText': editTitleText}); - result.addAll({'editDescriptionText': editDescriptionText}); - - return result; - } - - factory AlbumViewerPageState.fromMap(Map map) { - return AlbumViewerPageState( - isEditAlbum: map['isEditAlbum'] ?? false, - editTitleText: map['editTitleText'] ?? '', - editDescriptionText: map['editDescriptionText'] ?? '', - ); - } - - String toJson() => json.encode(toMap()); - - factory AlbumViewerPageState.fromJson(String source) => AlbumViewerPageState.fromMap(json.decode(source)); - - @override - String toString() => - 'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText, editDescriptionText: $editDescriptionText)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is AlbumViewerPageState && - other.isEditAlbum == isEditAlbum && - other.editTitleText == editTitleText && - other.editDescriptionText == editDescriptionText; - } - - @override - int get hashCode => isEditAlbum.hashCode ^ editTitleText.hashCode ^ editDescriptionText.hashCode; -} diff --git a/mobile/lib/models/albums/asset_selection_page_result.model.dart b/mobile/lib/models/albums/asset_selection_page_result.model.dart deleted file mode 100644 index cc750f397f..0000000000 --- a/mobile/lib/models/albums/asset_selection_page_result.model.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class AssetSelectionPageResult { - final Set selectedAssets; - - const AssetSelectionPageResult({required this.selectedAssets}); - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final setEquals = const DeepCollectionEquality().equals; - - return other is AssetSelectionPageResult && setEquals(other.selectedAssets, selectedAssets); - } - - @override - int get hashCode => selectedAssets.hashCode; -} diff --git a/mobile/lib/models/asset_selection_state.dart b/mobile/lib/models/asset_selection_state.dart deleted file mode 100644 index aded3064ce..0000000000 --- a/mobile/lib/models/asset_selection_state.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -class AssetSelectionState { - final bool hasRemote; - final bool hasLocal; - final bool hasMerged; - final int selectedCount; - - const AssetSelectionState({ - this.hasRemote = false, - this.hasLocal = false, - this.hasMerged = false, - this.selectedCount = 0, - }); - - AssetSelectionState copyWith({bool? hasRemote, bool? hasLocal, bool? hasMerged, int? selectedCount}) { - return AssetSelectionState( - hasRemote: hasRemote ?? this.hasRemote, - hasLocal: hasLocal ?? this.hasLocal, - hasMerged: hasMerged ?? this.hasMerged, - selectedCount: selectedCount ?? this.selectedCount, - ); - } - - AssetSelectionState.fromSelection(Set selection) - : hasLocal = selection.any((e) => e.storage == AssetState.local), - hasMerged = selection.any((e) => e.storage == AssetState.merged), - hasRemote = selection.any((e) => e.storage == AssetState.remote), - selectedCount = selection.length; - - @override - String toString() => - 'SelectionAssetState(hasRemote: $hasRemote, hasLocal: $hasLocal, hasMerged: $hasMerged, selectedCount: $selectedCount)'; - - @override - bool operator ==(covariant AssetSelectionState other) { - if (identical(this, other)) return true; - - return other.hasRemote == hasRemote && - other.hasLocal == hasLocal && - other.hasMerged == hasMerged && - other.selectedCount == selectedCount; - } - - @override - int get hashCode => hasRemote.hashCode ^ hasLocal.hashCode ^ hasMerged.hashCode ^ selectedCount.hashCode; -} diff --git a/mobile/lib/models/backup/available_album.model.dart b/mobile/lib/models/backup/available_album.model.dart deleted file mode 100644 index 502d0b66be..0000000000 --- a/mobile/lib/models/backup/available_album.model.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:immich_mobile/entities/album.entity.dart'; - -class AvailableAlbum { - final Album album; - final int assetCount; - final DateTime? lastBackup; - const AvailableAlbum({required this.album, required this.assetCount, this.lastBackup}); - - AvailableAlbum copyWith({Album? album, int? assetCount, DateTime? lastBackup}) { - return AvailableAlbum( - album: album ?? this.album, - assetCount: assetCount ?? this.assetCount, - lastBackup: lastBackup ?? this.lastBackup, - ); - } - - String get name => album.name; - - String get id => album.localId!; - - bool get isAll => album.isAll; - - @override - String toString() => 'AvailableAlbum(albumEntity: $album, lastBackup: $lastBackup)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is AvailableAlbum && other.album == album; - } - - @override - int get hashCode => album.hashCode; -} diff --git a/mobile/lib/models/backup/backup_candidate.model.dart b/mobile/lib/models/backup/backup_candidate.model.dart deleted file mode 100644 index 01c257dc05..0000000000 --- a/mobile/lib/models/backup/backup_candidate.model.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -class BackupCandidate { - BackupCandidate({required this.asset, required this.albumNames}); - - Asset asset; - List albumNames; - - @override - int get hashCode => asset.hashCode; - - @override - bool operator ==(Object other) { - if (other is! BackupCandidate) { - return false; - } - return asset == other.asset; - } -} diff --git a/mobile/lib/models/backup/backup_state.model.dart b/mobile/lib/models/backup/backup_state.model.dart deleted file mode 100644 index 51a17de4fc..0000000000 --- a/mobile/lib/models/backup/backup_state.model.dart +++ /dev/null @@ -1,173 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first - -import 'package:collection/collection.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; - -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; - -enum BackUpProgressEnum { idle, inProgress, manualInProgress, inBackground, done } - -class BackUpState { - // enum - final BackUpProgressEnum backupProgress; - final List allAssetsInDatabase; - final double progressInPercentage; - final String progressInFileSize; - final double progressInFileSpeed; - final List progressInFileSpeeds; - final DateTime progressInFileSpeedUpdateTime; - final int progressInFileSpeedUpdateSentBytes; - final double iCloudDownloadProgress; - final ServerDiskInfo serverInfo; - final bool autoBackup; - final bool backgroundBackup; - final bool backupRequireWifi; - final bool backupRequireCharging; - final int backupTriggerDelay; - - /// All available albums on the device - final List availableAlbums; - final Set selectedBackupAlbums; - final Set excludedBackupAlbums; - - /// Assets that are not overlapping in selected backup albums and excluded backup albums - final Set allUniqueAssets; - - /// All assets from the selected albums that have been backup - final Set selectedAlbumsBackupAssetsIds; - - // Current Backup Asset - final CurrentUploadAsset currentUploadAsset; - - const BackUpState({ - required this.backupProgress, - required this.allAssetsInDatabase, - required this.progressInPercentage, - required this.progressInFileSize, - required this.progressInFileSpeed, - required this.progressInFileSpeeds, - required this.progressInFileSpeedUpdateTime, - required this.progressInFileSpeedUpdateSentBytes, - required this.iCloudDownloadProgress, - required this.serverInfo, - required this.autoBackup, - required this.backgroundBackup, - required this.backupRequireWifi, - required this.backupRequireCharging, - required this.backupTriggerDelay, - required this.availableAlbums, - required this.selectedBackupAlbums, - required this.excludedBackupAlbums, - required this.allUniqueAssets, - required this.selectedAlbumsBackupAssetsIds, - required this.currentUploadAsset, - }); - - BackUpState copyWith({ - BackUpProgressEnum? backupProgress, - List? allAssetsInDatabase, - double? progressInPercentage, - String? progressInFileSize, - double? progressInFileSpeed, - List? progressInFileSpeeds, - DateTime? progressInFileSpeedUpdateTime, - int? progressInFileSpeedUpdateSentBytes, - double? iCloudDownloadProgress, - ServerDiskInfo? serverInfo, - bool? autoBackup, - bool? backgroundBackup, - bool? backupRequireWifi, - bool? backupRequireCharging, - int? backupTriggerDelay, - List? availableAlbums, - Set? selectedBackupAlbums, - Set? excludedBackupAlbums, - Set? allUniqueAssets, - Set? selectedAlbumsBackupAssetsIds, - CurrentUploadAsset? currentUploadAsset, - }) { - return BackUpState( - backupProgress: backupProgress ?? this.backupProgress, - allAssetsInDatabase: allAssetsInDatabase ?? this.allAssetsInDatabase, - progressInPercentage: progressInPercentage ?? this.progressInPercentage, - progressInFileSize: progressInFileSize ?? this.progressInFileSize, - progressInFileSpeed: progressInFileSpeed ?? this.progressInFileSpeed, - progressInFileSpeeds: progressInFileSpeeds ?? this.progressInFileSpeeds, - progressInFileSpeedUpdateTime: progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, - progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? this.progressInFileSpeedUpdateSentBytes, - iCloudDownloadProgress: iCloudDownloadProgress ?? this.iCloudDownloadProgress, - serverInfo: serverInfo ?? this.serverInfo, - autoBackup: autoBackup ?? this.autoBackup, - backgroundBackup: backgroundBackup ?? this.backgroundBackup, - backupRequireWifi: backupRequireWifi ?? this.backupRequireWifi, - backupRequireCharging: backupRequireCharging ?? this.backupRequireCharging, - backupTriggerDelay: backupTriggerDelay ?? this.backupTriggerDelay, - availableAlbums: availableAlbums ?? this.availableAlbums, - selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums, - excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums, - allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets, - selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds, - currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset, - ); - } - - @override - String toString() { - return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, progressInFileSize: $progressInFileSize, progressInFileSpeed: $progressInFileSpeed, progressInFileSpeeds: $progressInFileSpeeds, progressInFileSpeedUpdateTime: $progressInFileSpeedUpdateTime, progressInFileSpeedUpdateSentBytes: $progressInFileSpeedUpdateSentBytes, iCloudDownloadProgress: $iCloudDownloadProgress, serverInfo: $serverInfo, autoBackup: $autoBackup, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)'; - } - - @override - bool operator ==(covariant BackUpState other) { - if (identical(this, other)) return true; - final collectionEquals = const DeepCollectionEquality().equals; - - return other.backupProgress == backupProgress && - collectionEquals(other.allAssetsInDatabase, allAssetsInDatabase) && - other.progressInPercentage == progressInPercentage && - other.progressInFileSize == progressInFileSize && - other.progressInFileSpeed == progressInFileSpeed && - collectionEquals(other.progressInFileSpeeds, progressInFileSpeeds) && - other.progressInFileSpeedUpdateTime == progressInFileSpeedUpdateTime && - other.progressInFileSpeedUpdateSentBytes == progressInFileSpeedUpdateSentBytes && - other.iCloudDownloadProgress == iCloudDownloadProgress && - other.serverInfo == serverInfo && - other.autoBackup == autoBackup && - other.backgroundBackup == backgroundBackup && - other.backupRequireWifi == backupRequireWifi && - other.backupRequireCharging == backupRequireCharging && - other.backupTriggerDelay == backupTriggerDelay && - collectionEquals(other.availableAlbums, availableAlbums) && - collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) && - collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) && - collectionEquals(other.allUniqueAssets, allUniqueAssets) && - collectionEquals(other.selectedAlbumsBackupAssetsIds, selectedAlbumsBackupAssetsIds) && - other.currentUploadAsset == currentUploadAsset; - } - - @override - int get hashCode { - return backupProgress.hashCode ^ - allAssetsInDatabase.hashCode ^ - progressInPercentage.hashCode ^ - progressInFileSize.hashCode ^ - progressInFileSpeed.hashCode ^ - progressInFileSpeeds.hashCode ^ - progressInFileSpeedUpdateTime.hashCode ^ - progressInFileSpeedUpdateSentBytes.hashCode ^ - iCloudDownloadProgress.hashCode ^ - serverInfo.hashCode ^ - autoBackup.hashCode ^ - backgroundBackup.hashCode ^ - backupRequireWifi.hashCode ^ - backupRequireCharging.hashCode ^ - backupTriggerDelay.hashCode ^ - availableAlbums.hashCode ^ - selectedBackupAlbums.hashCode ^ - excludedBackupAlbums.hashCode ^ - allUniqueAssets.hashCode ^ - selectedAlbumsBackupAssetsIds.hashCode ^ - currentUploadAsset.hashCode; - } -} diff --git a/mobile/lib/models/backup/current_upload_asset.model.dart b/mobile/lib/models/backup/current_upload_asset.model.dart deleted file mode 100644 index 2214897357..0000000000 --- a/mobile/lib/models/backup/current_upload_asset.model.dart +++ /dev/null @@ -1,95 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; - -class CurrentUploadAsset { - final String id; - final DateTime fileCreatedAt; - final String fileName; - final String fileType; - final int? fileSize; - final bool? iCloudAsset; - - const CurrentUploadAsset({ - required this.id, - required this.fileCreatedAt, - required this.fileName, - required this.fileType, - this.fileSize, - this.iCloudAsset, - }); - - @pragma('vm:prefer-inline') - bool get isIcloudAsset => iCloudAsset != null && iCloudAsset!; - - CurrentUploadAsset copyWith({ - String? id, - DateTime? fileCreatedAt, - String? fileName, - String? fileType, - int? fileSize, - bool? iCloudAsset, - }) { - return CurrentUploadAsset( - id: id ?? this.id, - fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, - fileName: fileName ?? this.fileName, - fileType: fileType ?? this.fileType, - fileSize: fileSize ?? this.fileSize, - iCloudAsset: iCloudAsset ?? this.iCloudAsset, - ); - } - - Map toMap() { - return { - 'id': id, - 'fileCreatedAt': fileCreatedAt.millisecondsSinceEpoch, - 'fileName': fileName, - 'fileType': fileType, - 'fileSize': fileSize, - 'iCloudAsset': iCloudAsset, - }; - } - - factory CurrentUploadAsset.fromMap(Map map) { - return CurrentUploadAsset( - id: map['id'] as String, - fileCreatedAt: DateTime.fromMillisecondsSinceEpoch(map['fileCreatedAt'] as int), - fileName: map['fileName'] as String, - fileType: map['fileType'] as String, - fileSize: map['fileSize'] as int, - iCloudAsset: map['iCloudAsset'] != null ? map['iCloudAsset'] as bool : null, - ); - } - - String toJson() => json.encode(toMap()); - - factory CurrentUploadAsset.fromJson(String source) => - CurrentUploadAsset.fromMap(json.decode(source) as Map); - - @override - String toString() { - return 'CurrentUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, fileSize: $fileSize, iCloudAsset: $iCloudAsset)'; - } - - @override - bool operator ==(covariant CurrentUploadAsset other) { - if (identical(this, other)) return true; - - return other.id == id && - other.fileCreatedAt == fileCreatedAt && - other.fileName == fileName && - other.fileType == fileType && - other.fileSize == fileSize && - other.iCloudAsset == iCloudAsset; - } - - @override - int get hashCode { - return id.hashCode ^ - fileCreatedAt.hashCode ^ - fileName.hashCode ^ - fileType.hashCode ^ - fileSize.hashCode ^ - iCloudAsset.hashCode; - } -} diff --git a/mobile/lib/models/backup/error_upload_asset.model.dart b/mobile/lib/models/backup/error_upload_asset.model.dart deleted file mode 100644 index 38f241e748..0000000000 --- a/mobile/lib/models/backup/error_upload_asset.model.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -class ErrorUploadAsset { - final String id; - final DateTime fileCreatedAt; - final String fileName; - final String fileType; - final Asset asset; - final String errorMessage; - - const ErrorUploadAsset({ - required this.id, - required this.fileCreatedAt, - required this.fileName, - required this.fileType, - required this.asset, - required this.errorMessage, - }); - - ErrorUploadAsset copyWith({ - String? id, - DateTime? fileCreatedAt, - String? fileName, - String? fileType, - Asset? asset, - String? errorMessage, - }) { - return ErrorUploadAsset( - id: id ?? this.id, - fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, - fileName: fileName ?? this.fileName, - fileType: fileType ?? this.fileType, - asset: asset ?? this.asset, - errorMessage: errorMessage ?? this.errorMessage, - ); - } - - @override - String toString() { - return 'ErrorUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, asset: $asset, errorMessage: $errorMessage)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is ErrorUploadAsset && - other.id == id && - other.fileCreatedAt == fileCreatedAt && - other.fileName == fileName && - other.fileType == fileType && - other.asset == asset && - other.errorMessage == errorMessage; - } - - @override - int get hashCode { - return id.hashCode ^ - fileCreatedAt.hashCode ^ - fileName.hashCode ^ - fileType.hashCode ^ - asset.hashCode ^ - errorMessage.hashCode; - } -} diff --git a/mobile/lib/models/backup/manual_upload_state.model.dart b/mobile/lib/models/backup/manual_upload_state.model.dart deleted file mode 100644 index 120327c611..0000000000 --- a/mobile/lib/models/backup/manual_upload_state.model.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:collection/collection.dart'; - -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; - -class ManualUploadState { - // Current Backup Asset - final CurrentUploadAsset currentUploadAsset; - final int currentAssetIndex; - - final bool showDetailedNotification; - - /// Manual Upload Stats - final int totalAssetsToUpload; - final int successfulUploads; - final double progressInPercentage; - final String progressInFileSize; - final double progressInFileSpeed; - final List progressInFileSpeeds; - final DateTime progressInFileSpeedUpdateTime; - final int progressInFileSpeedUpdateSentBytes; - - const ManualUploadState({ - required this.progressInPercentage, - required this.progressInFileSize, - required this.progressInFileSpeed, - required this.progressInFileSpeeds, - required this.progressInFileSpeedUpdateTime, - required this.progressInFileSpeedUpdateSentBytes, - required this.currentUploadAsset, - required this.totalAssetsToUpload, - required this.currentAssetIndex, - required this.successfulUploads, - required this.showDetailedNotification, - }); - - ManualUploadState copyWith({ - double? progressInPercentage, - String? progressInFileSize, - double? progressInFileSpeed, - List? progressInFileSpeeds, - DateTime? progressInFileSpeedUpdateTime, - int? progressInFileSpeedUpdateSentBytes, - CurrentUploadAsset? currentUploadAsset, - int? totalAssetsToUpload, - int? successfulUploads, - int? currentAssetIndex, - bool? showDetailedNotification, - }) { - return ManualUploadState( - progressInPercentage: progressInPercentage ?? this.progressInPercentage, - progressInFileSize: progressInFileSize ?? this.progressInFileSize, - progressInFileSpeed: progressInFileSpeed ?? this.progressInFileSpeed, - progressInFileSpeeds: progressInFileSpeeds ?? this.progressInFileSpeeds, - progressInFileSpeedUpdateTime: progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, - progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? this.progressInFileSpeedUpdateSentBytes, - currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset, - totalAssetsToUpload: totalAssetsToUpload ?? this.totalAssetsToUpload, - currentAssetIndex: currentAssetIndex ?? this.currentAssetIndex, - successfulUploads: successfulUploads ?? this.successfulUploads, - showDetailedNotification: showDetailedNotification ?? this.showDetailedNotification, - ); - } - - @override - String toString() { - return 'ManualUploadState(progressInPercentage: $progressInPercentage, progressInFileSize: $progressInFileSize, progressInFileSpeed: $progressInFileSpeed, progressInFileSpeeds: $progressInFileSpeeds, progressInFileSpeedUpdateTime: $progressInFileSpeedUpdateTime, progressInFileSpeedUpdateSentBytes: $progressInFileSpeedUpdateSentBytes, currentUploadAsset: $currentUploadAsset, totalAssetsToUpload: $totalAssetsToUpload, successfulUploads: $successfulUploads, currentAssetIndex: $currentAssetIndex, showDetailedNotification: $showDetailedNotification)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final collectionEquals = const DeepCollectionEquality().equals; - - return other is ManualUploadState && - other.progressInPercentage == progressInPercentage && - other.progressInFileSize == progressInFileSize && - other.progressInFileSpeed == progressInFileSpeed && - collectionEquals(other.progressInFileSpeeds, progressInFileSpeeds) && - other.progressInFileSpeedUpdateTime == progressInFileSpeedUpdateTime && - other.progressInFileSpeedUpdateSentBytes == progressInFileSpeedUpdateSentBytes && - other.currentUploadAsset == currentUploadAsset && - other.totalAssetsToUpload == totalAssetsToUpload && - other.currentAssetIndex == currentAssetIndex && - other.successfulUploads == successfulUploads && - other.showDetailedNotification == showDetailedNotification; - } - - @override - int get hashCode { - return progressInPercentage.hashCode ^ - progressInFileSize.hashCode ^ - progressInFileSpeed.hashCode ^ - progressInFileSpeeds.hashCode ^ - progressInFileSpeedUpdateTime.hashCode ^ - progressInFileSpeedUpdateSentBytes.hashCode ^ - currentUploadAsset.hashCode ^ - totalAssetsToUpload.hashCode ^ - currentAssetIndex.hashCode ^ - successfulUploads.hashCode ^ - showDetailedNotification.hashCode; - } -} diff --git a/mobile/lib/models/backup/success_upload_asset.model.dart b/mobile/lib/models/backup/success_upload_asset.model.dart deleted file mode 100644 index da1e104ba3..0000000000 --- a/mobile/lib/models/backup/success_upload_asset.model.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; - -class SuccessUploadAsset { - final BackupCandidate candidate; - final String remoteAssetId; - final bool isDuplicate; - - const SuccessUploadAsset({required this.candidate, required this.remoteAssetId, required this.isDuplicate}); - - SuccessUploadAsset copyWith({BackupCandidate? candidate, String? remoteAssetId, bool? isDuplicate}) { - return SuccessUploadAsset( - candidate: candidate ?? this.candidate, - remoteAssetId: remoteAssetId ?? this.remoteAssetId, - isDuplicate: isDuplicate ?? this.isDuplicate, - ); - } - - @override - String toString() => - 'SuccessUploadAsset(asset: $candidate, remoteAssetId: $remoteAssetId, isDuplicate: $isDuplicate)'; - - @override - bool operator ==(covariant SuccessUploadAsset other) { - if (identical(this, other)) return true; - - return other.candidate == candidate && other.remoteAssetId == remoteAssetId && other.isDuplicate == isDuplicate; - } - - @override - int get hashCode => candidate.hashCode ^ remoteAssetId.hashCode ^ isDuplicate.hashCode; -} diff --git a/mobile/lib/models/memories/memory.model.dart b/mobile/lib/models/memories/memory.model.dart deleted file mode 100644 index 8a9db5d51b..0000000000 --- a/mobile/lib/models/memories/memory.model.dart +++ /dev/null @@ -1,29 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first - -import 'package:collection/collection.dart'; - -import 'package:immich_mobile/entities/asset.entity.dart'; - -class Memory { - final String title; - final List assets; - const Memory({required this.title, required this.assets}); - - Memory copyWith({String? title, List? assets}) { - return Memory(title: title ?? this.title, assets: assets ?? this.assets); - } - - @override - String toString() => 'Memory(title: $title, assets: $assets)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return other is Memory && other.title == title && listEquals(other.assets, assets); - } - - @override - int get hashCode => title.hashCode ^ assets.hashCode; -} diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 1b730e0c68..16f3be4655 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -1,8 +1,8 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; class SearchLocationFilter { String? country; diff --git a/mobile/lib/models/search/search_result.model.dart b/mobile/lib/models/search/search_result.model.dart deleted file mode 100644 index 02553869bf..0000000000 --- a/mobile/lib/models/search/search_result.model.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:collection/collection.dart'; - -import 'package:immich_mobile/entities/asset.entity.dart'; - -class SearchResult { - final List assets; - final int? nextPage; - - const SearchResult({required this.assets, this.nextPage}); - - SearchResult copyWith({List? assets, int? nextPage}) { - return SearchResult(assets: assets ?? this.assets, nextPage: nextPage ?? this.nextPage); - } - - @override - String toString() => 'SearchResult(assets: $assets, nextPage: $nextPage)'; - - @override - bool operator ==(covariant SearchResult other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.assets, assets) && other.nextPage == nextPage; - } - - @override - int get hashCode => assets.hashCode ^ nextPage.hashCode; -} diff --git a/mobile/lib/models/search/search_result_page_state.model.dart b/mobile/lib/models/search/search_result_page_state.model.dart deleted file mode 100644 index 7c8a27b50c..0000000000 --- a/mobile/lib/models/search/search_result_page_state.model.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class SearchResultPageState { - final bool isLoading; - final bool isSuccess; - final bool isError; - final bool isSmart; - final List searchResult; - - const SearchResultPageState({ - required this.isLoading, - required this.isSuccess, - required this.isError, - required this.isSmart, - required this.searchResult, - }); - - SearchResultPageState copyWith({ - bool? isLoading, - bool? isSuccess, - bool? isError, - bool? isSmart, - List? searchResult, - }) { - return SearchResultPageState( - isLoading: isLoading ?? this.isLoading, - isSuccess: isSuccess ?? this.isSuccess, - isError: isError ?? this.isError, - isSmart: isSmart ?? this.isSmart, - searchResult: searchResult ?? this.searchResult, - ); - } - - @override - String toString() { - return 'SearchresultPageState(isLoading: $isLoading, isSuccess: $isSuccess, isError: $isError, isSmart: $isSmart, searchResult: $searchResult)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return other is SearchResultPageState && - other.isLoading == isLoading && - other.isSuccess == isSuccess && - other.isError == isError && - other.isSmart == isSmart && - listEquals(other.searchResult, searchResult); - } - - @override - int get hashCode { - return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ isSmart.hashCode ^ searchResult.hashCode; - } -} diff --git a/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart b/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart deleted file mode 100644 index f40ac9ccae..0000000000 --- a/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -@RoutePage() -class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget { - final Album album; - - const AlbumAdditionalSharedUserSelectionPage({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final AsyncValue> suggestedShareUsers = ref.watch(otherUsersProvider); - final sharedUsersList = useState>({}); - - addNewUsersHandler() { - context.maybePop(sharedUsersList.value.map((e) => e.id).toList()); - } - - buildTileIcon(UserDto user) { - if (sharedUsersList.value.contains(user)) { - return CircleAvatar(backgroundColor: context.primaryColor, child: const Icon(Icons.check_rounded, size: 25)); - } else { - return UserCircleAvatar(user: user); - } - } - - buildUserList(List users) { - List usersChip = []; - - for (var user in sharedUsersList.value) { - usersChip.add( - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Chip( - backgroundColor: context.primaryColor.withValues(alpha: 0.15), - label: Text(user.name, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), - ), - ), - ); - } - return ListView( - children: [ - Wrap(children: [...usersChip]), - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'suggestions'.tr(), - style: const TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold), - ), - ), - ListView.builder( - primary: false, - shrinkWrap: true, - itemBuilder: ((context, index) { - return ListTile( - leading: buildTileIcon(users[index]), - dense: true, - title: Text(users[index].name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - subtitle: Text(users[index].email, style: const TextStyle(fontSize: 12)), - onTap: () { - if (sharedUsersList.value.contains(users[index])) { - sharedUsersList.value = sharedUsersList.value - .where((selectedUser) => selectedUser.id != users[index].id) - .toSet(); - } else { - sharedUsersList.value = {...sharedUsersList.value, users[index]}; - } - }, - ); - }), - itemCount: users.length, - ), - ], - ); - } - - return Scaffold( - appBar: AppBar( - title: const Text('invite_to_album').tr(), - elevation: 0, - centerTitle: false, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - context.maybePop(null); - }, - ), - actions: [ - TextButton( - onPressed: sharedUsersList.value.isEmpty ? null : addNewUsersHandler, - child: const Text("add", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ), - ], - ), - body: suggestedShareUsers.widgetWhen( - onData: (users) { - for (var sharedUsers in album.sharedUsers) { - users.removeWhere((u) => u.id == sharedUsers.id || u.id == album.ownerId); - } - - return buildUserList(users); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_asset_selection.page.dart b/mobile/lib/pages/album/album_asset_selection.page.dart deleted file mode 100644 index ccc4c44d43..0000000000 --- a/mobile/lib/pages/album/album_asset_selection.page.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; - -@RoutePage() -class AlbumAssetSelectionPage extends HookConsumerWidget { - const AlbumAssetSelectionPage({super.key, required this.existingAssets, this.canDeselect = false}); - - final Set existingAssets; - final bool canDeselect; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetSelectionRenderList = ref.watch(assetSelectionTimelineProvider); - final selected = useState>(existingAssets); - final selectionEnabledHook = useState(true); - - Widget buildBody(RenderList renderList) { - return ImmichAssetGrid( - renderList: renderList, - listener: (active, assets) { - selectionEnabledHook.value = active; - selected.value = assets; - }, - selectionActive: true, - preselectedAssets: existingAssets, - canDeselect: canDeselect, - showMultiSelectIndicator: false, - ); - } - - return Scaffold( - appBar: AppBar( - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - AutoRouter.of(context).popForced(null); - }, - ), - title: selected.value.isEmpty - ? const Text('add_photos', style: TextStyle(fontSize: 18)).tr() - : const Text( - 'share_assets_selected', - style: TextStyle(fontSize: 18), - ).tr(namedArgs: {'count': selected.value.length.toString()}), - centerTitle: false, - actions: [ - if (selected.value.isNotEmpty || canDeselect) - TextButton( - onPressed: () { - var payload = AssetSelectionPageResult(selectedAssets: selected.value); - AutoRouter.of(context).popForced(payload); - }, - child: Text( - canDeselect ? "done" : "add", - style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), - ).tr(), - ), - ], - ), - body: assetSelectionRenderList.widgetWhen(onData: (data) => buildBody(data)), - ); - } -} diff --git a/mobile/lib/pages/album/album_control_button.dart b/mobile/lib/pages/album/album_control_button.dart deleted file mode 100644 index 578eb839a0..0000000000 --- a/mobile/lib/pages/album/album_control_button.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; - -class AlbumControlButton extends ConsumerWidget { - final void Function()? onAddPhotosPressed; - final void Function()? onAddUsersPressed; - - const AlbumControlButton({super.key, this.onAddPhotosPressed, this.onAddUsersPressed}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SizedBox( - height: 36, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - if (onAddPhotosPressed != null) - AlbumActionFilledButton( - key: const ValueKey('add_photos_button'), - iconData: Icons.add_photo_alternate_outlined, - onPressed: onAddPhotosPressed, - labelText: "add_photos".tr(), - ), - if (onAddUsersPressed != null) - AlbumActionFilledButton( - key: const ValueKey('add_users_button'), - iconData: Icons.person_add_alt_rounded, - onPressed: onAddUsersPressed, - labelText: "album_viewer_page_share_add_users".tr(), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_date_range.dart b/mobile/lib/pages/album/album_date_range.dart deleted file mode 100644 index dbfd9214f1..0000000000 --- a/mobile/lib/pages/album/album_date_range.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; - -class AlbumDateRange extends ConsumerWidget { - const AlbumDateRange({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final data = ref.watch( - currentAlbumProvider.select((album) { - if (album == null || album.assets.isEmpty) { - return null; - } - - final startDate = album.startDate; - final endDate = album.endDate; - if (startDate == null || endDate == null) { - return null; - } - return (startDate, endDate, album.shared); - }), - ); - - if (data == null) { - return const SizedBox(); - } - final (startDate, endDate, shared) = data; - - return Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - _getDateRangeText(startDate, endDate), - style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceVariant), - ), - ); - } - - @pragma('vm:prefer-inline') - String _getDateRangeText(DateTime startDate, DateTime endDate) { - if (startDate.day == endDate.day && startDate.month == endDate.month && startDate.year == endDate.year) { - return DateFormat.yMMMd().format(startDate); - } - - final String startDateText = (startDate.year == endDate.year ? DateFormat.MMMd() : DateFormat.yMMMd()).format( - startDate, - ); - final String endDateText = DateFormat.yMMMd().format(endDate); - return "$startDateText - $endDateText"; - } -} diff --git a/mobile/lib/pages/album/album_description.dart b/mobile/lib/pages/album/album_description.dart deleted file mode 100644 index 383367e8b7..0000000000 --- a/mobile/lib/pages/album/album_description.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; - -class AlbumDescription extends ConsumerWidget { - const AlbumDescription({super.key, required this.descriptionFocusNode}); - - final FocusNode descriptionFocusNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final userId = ref.watch(authProvider).userId; - final (isOwner, isRemote, albumDescription) = ref.watch( - currentAlbumProvider.select((album) { - if (album == null) { - return const (false, false, ''); - } - - return (album.ownerId == userId, album.isRemote, album.description); - }), - ); - - if (isOwner && isRemote) { - return Padding( - padding: const EdgeInsets.only(left: 8, right: 8), - child: AlbumViewerEditableDescription( - albumDescription: albumDescription ?? 'add_a_description'.tr(), - descriptionFocusNode: descriptionFocusNode, - ), - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 16, right: 8), - child: Text(albumDescription ?? 'add_a_description'.tr(), style: context.textTheme.bodyLarge), - ); - } -} diff --git a/mobile/lib/pages/album/album_options.page.dart b/mobile/lib/pages/album/album_options.page.dart deleted file mode 100644 index ca65a92a79..0000000000 --- a/mobile/lib/pages/album/album_options.page.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -@RoutePage() -class AlbumOptionsPage extends HookConsumerWidget { - const AlbumOptionsPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentAlbumProvider); - if (album == null) { - return const SizedBox(); - } - - final sharedUsers = useState(album.sharedUsers.map((u) => u.toDto()).toList()); - final owner = album.owner.value; - final userId = ref.watch(authProvider).userId; - final activityEnabled = useState(album.activityEnabled); - final isProcessing = useProcessingOverlay(); - final isOwner = owner?.id == userId; - - void showErrorMessage() { - context.pop(); - ImmichToast.show( - context: context, - msg: "shared_album_section_people_action_error".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - - void leaveAlbum() async { - isProcessing.value = true; - - try { - final isSuccess = await ref.read(albumProvider.notifier).leaveAlbum(album); - - if (isSuccess) { - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - } else { - showErrorMessage(); - } - } catch (_) { - showErrorMessage(); - } - - isProcessing.value = false; - } - - void removeUserFromAlbum(UserDto user) async { - isProcessing.value = true; - - try { - await ref.read(albumProvider.notifier).removeUser(album, user); - album.sharedUsers.remove(entity.User.fromDto(user)); - sharedUsers.value = album.sharedUsers.map((u) => u.toDto()).toList(); - } catch (error) { - showErrorMessage(); - } - - context.pop(); - isProcessing.value = false; - } - - void handleUserClick(UserDto user) { - var actions = []; - - if (user.id == userId) { - actions = [ - ListTile( - leading: const Icon(Icons.exit_to_app_rounded), - title: const Text("shared_album_section_people_action_leave").tr(), - onTap: leaveAlbum, - ), - ]; - } - - if (isOwner) { - actions = [ - ListTile( - leading: const Icon(Icons.person_remove_rounded), - title: const Text("shared_album_section_people_action_remove_user").tr(), - onTap: () => removeUserFromAlbum(user), - ), - ]; - } - - showModalBottomSheet( - backgroundColor: context.colorScheme.surfaceContainer, - isScrollControlled: false, - context: context, - builder: (context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column(mainAxisSize: MainAxisSize.min, children: [...actions]), - ), - ); - }, - ); - } - - buildOwnerInfo() { - return ListTile( - leading: owner != null ? UserCircleAvatar(user: owner.toDto()) : const SizedBox(), - title: Text(album.owner.value?.name ?? "", style: const TextStyle(fontWeight: FontWeight.w500)), - subtitle: Text(album.owner.value?.email ?? "", style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), - trailing: Text("owner", style: context.textTheme.labelLarge).tr(), - ); - } - - buildSharedUsersList() { - return ListView.builder( - primary: false, - shrinkWrap: true, - itemCount: sharedUsers.value.length, - itemBuilder: (context, index) { - final user = sharedUsers.value[index]; - return ListTile( - leading: UserCircleAvatar(user: user), - title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)), - subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), - trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(), - onTap: userId == user.id || isOwner ? () => handleUserClick(user) : null, - ); - }, - ); - } - - buildSectionTitle(String text) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Text(text, style: context.textTheme.bodySmall), - ); - } - - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios_new_rounded), - onPressed: () => context.maybePop(null), - ), - centerTitle: true, - title: Text("options".tr()), - ), - body: ListView( - children: [ - if (isOwner && album.shared) - SwitchListTile.adaptive( - value: activityEnabled.value, - onChanged: (bool value) async { - activityEnabled.value = value; - if (await ref.read(albumProvider.notifier).setActivitystatus(album, value)) { - album.activityEnabled = value; - } - }, - activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor, - dense: true, - title: Text( - "comments_and_likes", - style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), - ).tr(), - subtitle: Text( - "let_others_respond", - style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), - ), - buildSectionTitle("shared_album_section_people_title".tr()), - buildOwnerInfo(), - buildSharedUsersList(), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_shared_user_icons.dart b/mobile/lib/pages/album/album_shared_user_icons.dart deleted file mode 100644 index 7cf6f387ae..0000000000 --- a/mobile/lib/pages/album/album_shared_user_icons.dart +++ /dev/null @@ -1,52 +0,0 @@ -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/domain/models/user.model.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class AlbumSharedUserIcons extends HookConsumerWidget { - const AlbumSharedUserIcons({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final sharedUsers = useRef>(const []); - sharedUsers.value = ref.watch( - currentAlbumProvider.select((album) { - if (album == null) { - return const []; - } - - if (album.sharedUsers.length == sharedUsers.value.length) { - return sharedUsers.value; - } - - return album.sharedUsers.map((u) => u.toDto()).toList(growable: false); - }), - ); - - if (sharedUsers.value.isEmpty) { - return const SizedBox(); - } - - return GestureDetector( - onTap: () => context.pushRoute(const AlbumOptionsRoute()), - child: SizedBox( - height: 50, - child: ListView.builder( - padding: const EdgeInsets.only(left: 16, bottom: 8), - scrollDirection: Axis.horizontal, - itemBuilder: ((context, index) { - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: UserCircleAvatar(user: sharedUsers.value[index], size: 36), - ); - }), - itemCount: sharedUsers.value.length, - ), - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_shared_user_selection.page.dart b/mobile/lib/pages/album/album_shared_user_selection.page.dart deleted file mode 100644 index ec084b1859..0000000000 --- a/mobile/lib/pages/album/album_shared_user_selection.page.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_title.provider.dart'; -import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -@RoutePage() -class AlbumSharedUserSelectionPage extends HookConsumerWidget { - const AlbumSharedUserSelectionPage({super.key, required this.assets}); - - final Set assets; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final sharedUsersList = useState>({}); - final suggestedShareUsers = ref.watch(otherUsersProvider); - - createSharedAlbum() async { - var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(ref.watch(albumTitleProvider), assets); - - if (newAlbum != null) { - ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); - unawaited(context.maybePop(true)); - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - } - - ScaffoldMessenger( - child: SnackBar( - content: Text( - 'select_user_for_sharing_page_err_album', - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ).tr(), - ), - ); - } - - buildTileIcon(UserDto user) { - if (sharedUsersList.value.contains(user)) { - return CircleAvatar(backgroundColor: context.primaryColor, child: const Icon(Icons.check_rounded, size: 25)); - } else { - return UserCircleAvatar(user: user); - } - } - - buildUserList(List users) { - List usersChip = []; - - for (var user in sharedUsersList.value) { - usersChip.add( - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Chip( - backgroundColor: context.primaryColor.withValues(alpha: 0.15), - label: Text( - user.email, - style: const TextStyle(fontSize: 12, color: Colors.black87, fontWeight: FontWeight.bold), - ), - ), - ), - ); - } - return ListView( - children: [ - Wrap(children: [...usersChip]), - Padding( - padding: const EdgeInsets.all(16.0), - child: const Text( - 'suggestions', - style: TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold), - ).tr(), - ), - ListView.builder( - primary: false, - shrinkWrap: true, - itemBuilder: ((context, index) { - return ListTile( - leading: buildTileIcon(users[index]), - title: Text(users[index].email, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - onTap: () { - if (sharedUsersList.value.contains(users[index])) { - sharedUsersList.value = sharedUsersList.value - .where((selectedUser) => selectedUser.id != users[index].id) - .toSet(); - } else { - sharedUsersList.value = {...sharedUsersList.value, users[index]}; - } - }, - ); - }), - itemCount: users.length, - ), - ], - ); - } - - return Scaffold( - appBar: AppBar( - title: Text('invite_to_album', style: TextStyle(color: context.primaryColor)).tr(), - elevation: 0, - centerTitle: false, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - unawaited(context.maybePop()); - }, - ), - actions: [ - TextButton( - style: TextButton.styleFrom(foregroundColor: context.primaryColor), - onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum, - child: const Text( - "create_album", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - // color: context.primaryColor, - ), - ).tr(), - ), - ], - ), - body: suggestedShareUsers.widgetWhen( - onData: (users) { - return buildUserList(users); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_title.dart b/mobile/lib/pages/album/album_title.dart deleted file mode 100644 index 6c7fc3faaa..0000000000 --- a/mobile/lib/pages/album/album_title.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; - -class AlbumTitle extends ConsumerWidget { - const AlbumTitle({super.key, required this.titleFocusNode}); - - final FocusNode titleFocusNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final userId = ref.watch(authProvider).userId; - final (isOwner, isRemote, albumName) = ref.watch( - currentAlbumProvider.select((album) { - if (album == null) { - return const (false, false, ''); - } - - return (album.ownerId == userId, album.isRemote, album.name); - }), - ); - - if (isOwner && isRemote) { - return Padding( - padding: const EdgeInsets.only(left: 8, right: 8), - child: AlbumViewerEditableTitle(albumName: albumName, titleFocusNode: titleFocusNode), - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 16, right: 8), - child: Text(albumName, style: context.textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w700)), - ); - } -} diff --git a/mobile/lib/pages/album/album_viewer.dart b/mobile/lib/pages/album/album_viewer.dart deleted file mode 100644 index 97853fb96a..0000000000 --- a/mobile/lib/pages/album/album_viewer.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; -import 'package:immich_mobile/pages/album/album_control_button.dart'; -import 'package:immich_mobile/pages/album/album_date_range.dart'; -import 'package:immich_mobile/pages/album/album_description.dart'; -import 'package:immich_mobile/pages/album/album_shared_user_icons.dart'; -import 'package:immich_mobile/pages/album/album_title.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_appbar.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumViewer extends HookConsumerWidget { - const AlbumViewer({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentAlbumProvider); - if (album == null) { - return const SizedBox(); - } - - final titleFocusNode = useFocusNode(); - final descriptionFocusNode = useFocusNode(); - final userId = ref.watch(authProvider).userId; - final isMultiselecting = ref.watch(multiselectProvider); - final isProcessing = useProcessingOverlay(); - final isOwner = ref.watch( - currentAlbumProvider.select((album) { - return album?.ownerId == userId; - }), - ); - - Future onRemoveFromAlbumPressed(Iterable assets) async { - final bool isSuccess = await ref.read(albumProvider.notifier).removeAsset(album, assets); - - if (!isSuccess) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_remove".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - return isSuccess; - } - - /// Find out if the assets in album exist on the device - /// If they exist, add to selected asset state to show they are already selected. - void onAddPhotosPressed() async { - AssetSelectionPageResult? returnPayload = await context.pushRoute( - AlbumAssetSelectionRoute(existingAssets: album.assets, canDeselect: false), - ); - - if (returnPayload != null && returnPayload.selectedAssets.isNotEmpty) { - // Check if there is new assets add - isProcessing.value = true; - - await ref.watch(albumProvider.notifier).addAssets(album, returnPayload.selectedAssets); - - isProcessing.value = false; - } - } - - void onAddUsersPressed() async { - List? sharedUserIds = await context.pushRoute?>( - AlbumAdditionalSharedUserSelectionRoute(album: album), - ); - - if (sharedUserIds != null) { - isProcessing.value = true; - - await ref.watch(albumProvider.notifier).addUsers(album, sharedUserIds); - - isProcessing.value = false; - } - } - - onActivitiesPressed() { - if (album.remoteId != null) { - ref.read(currentAssetProvider.notifier).set(null); - context.pushRoute(const ActivitiesRoute()); - } - } - - return Stack( - children: [ - MultiselectGrid( - key: const ValueKey("albumViewerMultiselectGrid"), - renderListProvider: albumTimelineProvider(album.id), - topWidget: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - context.primaryColor.withValues(alpha: 0.06), - context.primaryColor.withValues(alpha: 0.04), - Colors.indigo.withValues(alpha: 0.02), - Colors.transparent, - ], - stops: const [0.0, 0.3, 0.7, 1.0], - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 32), - const AlbumDateRange(), - AlbumTitle(key: const ValueKey("albumTitle"), titleFocusNode: titleFocusNode), - AlbumDescription(key: const ValueKey("albumDescription"), descriptionFocusNode: descriptionFocusNode), - const AlbumSharedUserIcons(), - if (album.isRemote) - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: AlbumControlButton( - key: const ValueKey("albumControlButton"), - onAddPhotosPressed: onAddPhotosPressed, - onAddUsersPressed: isOwner ? onAddUsersPressed : null, - ), - ), - const SizedBox(height: 8), - ], - ), - ), - onRemoveFromAlbum: onRemoveFromAlbumPressed, - editEnabled: album.ownerId == userId, - ), - AnimatedPositioned( - key: const ValueKey("albumViewerAppbarPositioned"), - duration: const Duration(milliseconds: 300), - top: isMultiselecting ? -(kToolbarHeight + context.padding.top) : 0, - left: 0, - right: 0, - child: AlbumViewerAppbar( - key: const ValueKey("albumViewerAppbar"), - titleFocusNode: titleFocusNode, - descriptionFocusNode: descriptionFocusNode, - userId: userId, - onAddPhotos: onAddPhotosPressed, - onAddUsers: onAddUsersPressed, - onActivities: onActivitiesPressed, - ), - ), - ], - ); - } -} diff --git a/mobile/lib/pages/album/album_viewer.page.dart b/mobile/lib/pages/album/album_viewer.page.dart deleted file mode 100644 index c99dacd9b7..0000000000 --- a/mobile/lib/pages/album/album_viewer.page.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/pages/album/album_viewer.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; - -@RoutePage() -class AlbumViewerPage extends HookConsumerWidget { - final int albumId; - - const AlbumViewerPage({super.key, required this.albumId}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Listen provider to prevent autoDispose when navigating to other routes from within the viewer page - ref.listen(currentAlbumProvider, (_, __) {}); - - // This call helps rendering the asset selection instantly - ref.listen(assetSelectionTimelineProvider, (_, __) {}); - - ref.listen(albumWatcher(albumId), (_, albumFuture) { - albumFuture.whenData((value) => ref.read(currentAlbumProvider.notifier).set(value)); - }); - - return const Scaffold(body: AlbumViewer()); - } -} diff --git a/mobile/lib/pages/albums/albums.page.dart b/mobile/lib/pages/albums/albums.page.dart deleted file mode 100644 index 5f155c2f0d..0000000000 --- a/mobile/lib/pages/albums/albums.page.dart +++ /dev/null @@ -1,359 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; -import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; - -@RoutePage() -class AlbumsPage extends HookConsumerWidget { - const AlbumsPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(albumProvider).where((album) => album.isRemote).toList(); - final albumSortOption = ref.watch(albumSortByOptionsProvider); - final albumSortIsReverse = ref.watch(albumSortOrderProvider); - final sorted = albumSortOption.sortFn(albums, albumSortIsReverse); - final isGrid = useState(false); - final searchController = useTextEditingController(); - final debounceTimer = useRef(null); - final filterMode = useState(QuickFilterMode.all); - final userId = ref.watch(currentUserProvider)?.id; - final searchFocusNode = useFocusNode(); - - toggleViewMode() { - isGrid.value = !isGrid.value; - } - - onSearch(String searchTerm, QuickFilterMode mode) { - debounceTimer.value?.cancel(); - debounceTimer.value = Timer(const Duration(milliseconds: 300), () { - ref.read(albumProvider.notifier).searchAlbums(searchTerm, mode); - }); - } - - changeFilter(QuickFilterMode mode) { - filterMode.value = mode; - } - - useEffect(() { - searchController.addListener(() { - onSearch(searchController.text, filterMode.value); - }); - - return () { - searchController.removeListener(() { - onSearch(searchController.text, filterMode.value); - }); - debounceTimer.value?.cancel(); - }; - }, []); - - clearSearch() { - filterMode.value = QuickFilterMode.all; - searchController.clear(); - onSearch('', QuickFilterMode.all); - } - - return Scaffold( - appBar: ImmichAppBar( - showUploadButton: false, - actions: [ - IconButton( - icon: const Icon(Icons.add_rounded, size: 28), - onPressed: () => context.pushRoute(CreateAlbumRoute()), - ), - ], - ), - body: RefreshIndicator( - displacement: 70, - onRefresh: () async { - await ref.read(albumProvider.notifier).refreshRemoteAlbums(); - }, - child: ListView( - shrinkWrap: true, - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12), - children: [ - Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0), - borderRadius: const BorderRadius.all(Radius.circular(24)), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withValues(alpha: 0.075), - context.colorScheme.primary.withValues(alpha: 0.09), - context.colorScheme.primary.withValues(alpha: 0.075), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - transform: const GradientRotation(0.5 * pi), - ), - ), - child: SearchField( - autofocus: false, - contentPadding: const EdgeInsets.all(16), - hintText: 'search_albums'.tr(), - prefixIcon: const Icon(Icons.search_rounded), - suffixIcon: searchController.text.isNotEmpty - ? IconButton(icon: const Icon(Icons.clear_rounded), onPressed: clearSearch) - : null, - controller: searchController, - onChanged: (_) => onSearch(searchController.text, filterMode.value), - focusNode: searchFocusNode, - onTapOutside: (_) => searchFocusNode.unfocus(), - ), - ), - const SizedBox(height: 8), - Wrap( - spacing: 4, - runSpacing: 4, - children: [ - QuickFilterButton( - label: 'all'.tr(), - isSelected: filterMode.value == QuickFilterMode.all, - onTap: () { - changeFilter(QuickFilterMode.all); - onSearch(searchController.text, QuickFilterMode.all); - }, - ), - QuickFilterButton( - label: 'shared_with_me'.tr(), - isSelected: filterMode.value == QuickFilterMode.sharedWithMe, - onTap: () { - changeFilter(QuickFilterMode.sharedWithMe); - onSearch(searchController.text, QuickFilterMode.sharedWithMe); - }, - ), - QuickFilterButton( - label: 'my_albums'.tr(), - isSelected: filterMode.value == QuickFilterMode.myAlbums, - onTap: () { - changeFilter(QuickFilterMode.myAlbums); - onSearch(searchController.text, QuickFilterMode.myAlbums); - }, - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SortButton(), - IconButton( - icon: Icon(isGrid.value ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24), - onPressed: toggleViewMode, - ), - ], - ), - const SizedBox(height: 5), - AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: isGrid.value - ? GridView.builder( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 250, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - childAspectRatio: .7, - ), - itemBuilder: (context, index) { - return AlbumThumbnailCard( - album: sorted[index], - onTap: () => context.pushRoute(AlbumViewerRoute(albumId: sorted[index].id)), - showOwner: true, - ); - }, - itemCount: sorted.length, - ) - : ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: sorted.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: LargeLeadingTile( - title: Text( - sorted[index].name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), - ), - subtitle: sorted[index].ownerId != null - ? Text( - '${'items_count'.t(context: context, args: {'count': sorted[index].assetCount})} â€ĸ ${sorted[index].ownerId != userId ? 'shared_by_user'.t(context: context, args: {'user': sorted[index].ownerName!}) : 'owned'.t(context: context)}', - overflow: TextOverflow.ellipsis, - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSurfaceSecondary, - ), - ) - : null, - onTap: () => context.pushRoute(AlbumViewerRoute(albumId: sorted[index].id)), - leadingPadding: const EdgeInsets.only(right: 16), - leading: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(15)), - child: ImmichThumbnail(asset: sorted[index].thumbnail.value, width: 80, height: 80), - ), - // minVerticalPadding: 1, - ), - ); - }, - ), - ), - ], - ), - ), - resizeToAvoidBottomInset: false, - ); - } -} - -class QuickFilterButton extends StatelessWidget { - const QuickFilterButton({super.key, required this.isSelected, required this.onTap, required this.label}); - - final bool isSelected; - final VoidCallback onTap; - final String label; - - @override - Widget build(BuildContext context) { - return TextButton( - onPressed: onTap, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(isSelected ? context.colorScheme.primary : Colors.transparent), - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(20)), - side: BorderSide(color: context.colorScheme.onSurface.withAlpha(25), width: 1), - ), - ), - ), - child: Text( - label, - style: TextStyle( - color: isSelected ? context.colorScheme.onPrimary : context.colorScheme.onSurface, - fontSize: 14, - ), - ), - ); - } -} - -class SortButton extends ConsumerWidget { - const SortButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumSortOption = ref.watch(albumSortByOptionsProvider); - final albumSortIsReverse = ref.watch(albumSortOrderProvider); - - return MenuAnchor( - style: MenuStyle( - elevation: const WidgetStatePropertyAll(1), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - ), - padding: const WidgetStatePropertyAll(EdgeInsets.all(4)), - ), - consumeOutsideTap: true, - menuChildren: AlbumSortMode.values - .map( - (mode) => MenuItemButton( - leadingIcon: albumSortOption == mode - ? albumSortIsReverse - ? Icon( - Icons.keyboard_arrow_down, - color: albumSortOption == mode - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface, - ) - : Icon( - Icons.keyboard_arrow_up_rounded, - color: albumSortOption == mode - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface, - ) - : const Icon(Icons.abc, color: Colors.transparent), - onPressed: () { - final selected = albumSortOption == mode; - // Switch direction - if (selected) { - ref.read(albumSortOrderProvider.notifier).changeSortDirection(!albumSortIsReverse); - } else { - ref.read(albumSortByOptionsProvider.notifier).changeSortMode(mode); - } - }, - style: ButtonStyle( - padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(16, 16, 32, 16)), - backgroundColor: WidgetStateProperty.all( - albumSortOption == mode ? context.colorScheme.primary : Colors.transparent, - ), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - ), - ), - child: Text( - mode.label.tr(), - style: context.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - color: albumSortOption == mode - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface.withAlpha(185), - ), - ), - ), - ) - .toList(), - builder: (context, controller, child) { - return GestureDetector( - onTap: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 5), - child: Transform.rotate( - angle: 90 * pi / 180, - child: Icon( - Icons.compare_arrows_rounded, - size: 18, - color: context.colorScheme.onSurface.withAlpha(225), - ), - ), - ), - Text( - albumSortOption.label.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: context.colorScheme.onSurface.withAlpha(225), - ), - ), - ], - ), - ); - }, - ); - } -} diff --git a/mobile/lib/pages/backup/album_preview.page.dart b/mobile/lib/pages/backup/album_preview.page.dart deleted file mode 100644 index def31afcd4..0000000000 --- a/mobile/lib/pages/backup/album_preview.page.dart +++ /dev/null @@ -1,64 +0,0 @@ -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/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -@RoutePage() -class AlbumPreviewPage extends HookConsumerWidget { - final Album album; - const AlbumPreviewPage({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assets = useState>([]); - - getAssetsInAlbum() async { - assets.value = await ref.read(albumMediaRepositoryProvider).getAssets(album.localId!); - } - - useEffect(() { - getAssetsInAlbum(); - return null; - }, []); - - return Scaffold( - appBar: AppBar( - elevation: 0, - title: Column( - children: [ - Text(album.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text( - "ID ${album.id}", - style: TextStyle( - fontSize: 10, - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_new_rounded)), - ), - body: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 5, - crossAxisSpacing: 2, - mainAxisSpacing: 2, - ), - itemCount: assets.value.length, - itemBuilder: (context, index) { - return ImmichThumbnail(asset: assets.value[index], width: 100, height: 100); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart deleted file mode 100644 index d222211577..0000000000 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ /dev/null @@ -1,225 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/backup/album_info_card.dart'; -import 'package:immich_mobile/widgets/backup/album_info_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; - -@RoutePage() -class BackupAlbumSelectionPage extends HookConsumerWidget { - const BackupAlbumSelectionPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; - final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; - final enableSyncUploadAlbum = useAppSettingsState(AppSettingsEnum.syncAlbums); - final isDarkTheme = context.isDarkTheme; - final albums = ref.watch(backupProvider).availableAlbums; - - useEffect(() { - ref.watch(backupProvider.notifier).getBackupInfo(); - return null; - }, []); - - buildAlbumSelectionList() { - if (albums.isEmpty) { - return const SliverToBoxAdapter(child: Center(child: CircularProgressIndicator())); - } - - return SliverPadding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - sliver: SliverList( - delegate: SliverChildBuilderDelegate(((context, index) { - return AlbumInfoListTile(album: albums[index]); - }), childCount: albums.length), - ), - ); - } - - buildAlbumSelectionGrid() { - if (albums.isEmpty) { - return const SliverToBoxAdapter(child: Center(child: CircularProgressIndicator())); - } - - return SliverPadding( - padding: const EdgeInsets.all(12.0), - sliver: SliverGrid.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 300, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - ), - itemCount: albums.length, - itemBuilder: ((context, index) { - return AlbumInfoCard(album: albums[index]); - }), - ), - ); - } - - buildSelectedAlbumNameChip() { - return selectedBackupAlbums.map((album) { - void removeSelection() => ref.read(backupProvider.notifier).removeAlbumForBackup(album); - - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: GestureDetector( - onTap: removeSelection, - child: Chip( - label: Text( - album.name, - style: TextStyle( - fontSize: 12, - color: isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - ), - backgroundColor: context.primaryColor, - deleteIconColor: isDarkTheme ? Colors.black : Colors.white, - deleteIcon: const Icon(Icons.cancel_rounded, size: 15), - onDeleted: removeSelection, - ), - ), - ); - }).toSet(); - } - - buildExcludedAlbumNameChip() { - return excludedBackupAlbums.map((album) { - void removeSelection() { - ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(album); - } - - return GestureDetector( - onTap: removeSelection, - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Chip( - label: Text( - album.name, - style: TextStyle(fontSize: 12, color: context.scaffoldBackgroundColor, fontWeight: FontWeight.bold), - ), - backgroundColor: Colors.red[300], - deleteIconColor: context.scaffoldBackgroundColor, - deleteIcon: const Icon(Icons.cancel_rounded, size: 15), - onDeleted: removeSelection, - ), - ), - ); - }).toSet(); - } - - 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 Scaffold( - appBar: AppBar( - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - title: const Text("backup_album_selection_page_select_albums").tr(), - elevation: 0, - ), - body: CustomScrollView( - physics: const ClampingScrollPhysics(), - slivers: [ - SliverToBoxAdapter( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Text("backup_album_selection_page_selection_info", style: context.textTheme.titleSmall).tr(), - ), - - // Selected Album Chips - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Wrap(children: [...buildSelectedAlbumNameChip(), ...buildExcludedAlbumNameChip()]), - ), - - SettingsSwitchListTile( - valueNotifier: enableSyncUploadAlbum, - title: "sync_albums".tr(), - subtitle: "sync_upload_album_setting_subtitle".tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 16), - titleStyle: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold), - subtitleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary), - onChanged: handleSyncAlbumToggle, - ), - - ListTile( - title: Text( - "backup_album_selection_page_albums_device".tr( - namedArgs: {'count': ref.watch(backupProvider).availableAlbums.length.toString()}, - ), - style: context.textTheme.titleSmall, - ), - subtitle: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - "backup_album_selection_page_albums_tap", - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ).tr(), - ), - trailing: IconButton( - splashRadius: 16, - icon: Icon(Icons.info, size: 20, color: context.primaryColor), - onPressed: () { - // show the dialog - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), - elevation: 5, - title: Text( - 'backup_album_selection_page_selection_info', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: context.primaryColor), - ).tr(), - content: SingleChildScrollView( - child: ListBody( - children: [ - const Text( - 'backup_album_selection_page_assets_scatter', - style: TextStyle(fontSize: 14), - ).tr(), - ], - ), - ), - ); - }, - ); - }, - ), - ), - - // buildSearchBar(), - ], - ), - ), - SliverLayoutBuilder( - builder: (context, constraints) { - if (constraints.crossAxisExtent > 600) { - return buildAlbumSelectionGrid(); - } else { - return buildAlbumSelectionList(); - } - }, - ), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart deleted file mode 100644 index 1e008be1bb..0000000000 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ /dev/null @@ -1,286 +0,0 @@ -import 'dart:io'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/backup/backup_info_card.dart'; -import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; - -@RoutePage() -class BackupControllerPage extends HookConsumerWidget { - const BackupControllerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - BackUpState backupState = ref.watch(backupProvider); - final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty; - final didGetBackupInfo = useState(false); - - bool hasExclusiveAccess = backupState.backupProgress != BackUpProgressEnum.inBackground; - bool shouldBackup = - backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length == 0 || - !hasExclusiveAccess - ? false - : true; - - useEffect(() { - // Update the background settings information just to make sure we - // have the latest, since the platform channel will not update - // automatically - if (Platform.isIOS) { - ref.watch(iOSBackgroundSettingsProvider.notifier).refresh(); - } - - ref.watch(websocketProvider.notifier).stopListenToEvent('on_upload_success'); - - return () { - WakelockPlus.disable(); - }; - }, []); - - useEffect(() { - if (backupState.backupProgress == BackUpProgressEnum.idle && !didGetBackupInfo.value) { - ref.watch(backupProvider.notifier).getBackupInfo(); - didGetBackupInfo.value = true; - } - return null; - }, [backupState.backupProgress]); - - useEffect(() { - if (backupState.backupProgress == BackUpProgressEnum.inProgress) { - WakelockPlus.enable(); - } else { - WakelockPlus.disable(); - } - - return null; - }, [backupState.backupProgress]); - - Widget buildSelectedAlbumName() { - var text = "backup_controller_page_backup_selected".tr(); - var albums = ref.watch(backupProvider).selectedBackupAlbums; - - if (albums.isNotEmpty) { - for (var album in albums) { - if (album.name == "Recent" || album.name == "Recents") { - text += "${album.name} (${'all'.tr()}), "; - } else { - text += "${album.name}, "; - } - } - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - text.trim().substring(0, text.length - 2), - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ), - ); - } else { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - "backup_controller_page_none_selected".tr(), - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ), - ); - } - } - - Widget buildExcludedAlbumName() { - var text = "backup_controller_page_excluded".tr(); - var albums = ref.watch(backupProvider).excludedBackupAlbums; - - if (albums.isNotEmpty) { - for (var album in albums) { - text += "${album.name}, "; - } - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - text.trim().substring(0, text.length - 2), - style: context.textTheme.labelLarge?.copyWith(color: Colors.red[300]), - ), - ); - } else { - return const SizedBox(); - } - } - - buildFolderSelectionTile() { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Card( - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(20)), - side: BorderSide(color: context.colorScheme.outlineVariant, width: 1), - ), - elevation: 0, - borderOnForeground: false, - child: ListTile( - minVerticalPadding: 18, - title: Text("backup_controller_page_albums", style: context.textTheme.titleMedium).tr(), - subtitle: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "backup_controller_page_to_backup", - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), - buildSelectedAlbumName(), - buildExcludedAlbumName(), - ], - ), - ), - trailing: ElevatedButton( - onPressed: () async { - await context.pushRoute(const BackupAlbumSelectionRoute()); - // waited until returning from selection - await ref.read(backupProvider.notifier).backupAlbumSelectionDone(); - // waited until backup albums are stored in DB - await ref.read(albumProvider.notifier).refreshDeviceAlbums(); - }, - child: const Text("select", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - ), - ), - ), - ); - } - - void startBackup() { - ref.watch(errorBackupListProvider.notifier).empty(); - if (ref.watch(backupProvider).backupProgress != BackUpProgressEnum.inBackground) { - ref.watch(backupProvider.notifier).startBackupProcess(); - } - } - - Widget buildBackupButton() { - return Padding( - padding: const EdgeInsets.only(top: 24), - child: Container( - child: - backupState.backupProgress == BackUpProgressEnum.inProgress || - backupState.backupProgress == BackUpProgressEnum.manualInProgress - ? ElevatedButton( - style: ElevatedButton.styleFrom( - foregroundColor: Colors.grey[50], - backgroundColor: Colors.red[300], - // padding: const EdgeInsets.all(14), - ), - onPressed: () { - if (backupState.backupProgress == BackUpProgressEnum.manualInProgress) { - ref.read(manualUploadProvider.notifier).cancelBackup(); - } else { - ref.read(backupProvider.notifier).cancelBackup(); - } - }, - child: const Text("cancel", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ) - : ElevatedButton( - onPressed: shouldBackup ? startBackup : null, - child: const Text( - "backup_controller_page_start_backup", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ).tr(), - ), - ), - ); - } - - buildBackgroundBackupInfo() { - return ListTile( - leading: const Icon(Icons.info_outline_rounded), - title: Text('background_backup_running_error'.tr()), - ); - } - - buildLoadingIndicator() { - return const Padding( - padding: EdgeInsets.only(top: 42.0), - child: Center(child: CircularProgressIndicator()), - ); - } - - return Scaffold( - appBar: AppBar( - elevation: 0, - title: const Text("backup_controller_page_backup").tr(), - leading: IconButton( - onPressed: () { - ref.watch(websocketProvider.notifier).listenUploadEvent(); - context.maybePop(true); - }, - splashRadius: 24, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: IconButton( - onPressed: () => context.pushRoute(const BackupOptionsRoute()), - splashRadius: 24, - icon: const Icon(Icons.settings_outlined), - ), - ), - ], - ), - body: Stack( - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), - child: ListView( - // crossAxisAlignment: CrossAxisAlignment.start, - children: hasAnyAlbum - ? [ - buildFolderSelectionTile(), - BackupInfoCard( - title: "total".tr(), - subtitle: "backup_controller_page_total_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${backupState.allUniqueAssets.length}", - ), - BackupInfoCard( - title: "backup_controller_page_backup".tr(), - subtitle: "backup_controller_page_backup_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${backupState.selectedAlbumsBackupAssetsIds.length}", - ), - BackupInfoCard( - title: "backup_controller_page_remainder".tr(), - subtitle: "backup_controller_page_remainder_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}", - ), - const Divider(), - const CurrentUploadingAssetInfoBox(), - if (!hasExclusiveAccess) buildBackgroundBackupInfo(), - buildBackupButton(), - ] - : [buildFolderSelectionTile(), if (!didGetBackupInfo.value) buildLoadingIndicator()], - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/backup/backup_options.page.dart b/mobile/lib/pages/backup/backup_options.page.dart deleted file mode 100644 index 846a32a742..0000000000 --- a/mobile/lib/pages/backup/backup_options.page.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; - -@RoutePage() -class BackupOptionsPage extends StatelessWidget { - const BackupOptionsPage({super.key}); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - elevation: 0, - title: const Text("backup_options_page_title").tr(), - leading: IconButton( - onPressed: () => context.maybePop(true), - splashRadius: 24, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - ), - body: const BackupSettings(), - ); - } -} diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index 3ba3389eea..6bdb8dd552 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -45,14 +45,17 @@ class _DriftBackupPageState extends ConsumerState { } WidgetsBinding.instance.addPostFrameCallback((_) async { - await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); + final backupNotifier = ref.read(driftBackupProvider.notifier); + final syncManager = ref.read(backgroundSyncProvider); - ref.read(driftBackupProvider.notifier).updateSyncing(true); - syncSuccess = await ref.read(backgroundSyncProvider).syncRemote(); - ref.read(driftBackupProvider.notifier).updateSyncing(false); + await backupNotifier.getBackupStatus(currentUser.id); + + backupNotifier.updateSyncing(true); + syncSuccess = await syncManager.syncRemote(); + backupNotifier.updateSyncing(false); if (mounted) { - await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); + await backupNotifier.getBackupStatus(currentUser.id); } }); } @@ -82,9 +85,9 @@ class _DriftBackupPageState extends ConsumerState { } if (syncSuccess == null) { - ref.read(driftBackupProvider.notifier).updateSyncing(true); + backupNotifier.updateSyncing(true); syncSuccess = await backupSyncManager.syncRemote(); - ref.read(driftBackupProvider.notifier).updateSyncing(false); + backupNotifier.updateSyncing(false); } await backupNotifier.getBackupStatus(currentUser.id); diff --git a/mobile/lib/pages/backup/failed_backup_status.page.dart b/mobile/lib/pages/backup/failed_backup_status.page.dart deleted file mode 100644 index a97a133b89..0000000000 --- a/mobile/lib/pages/backup/failed_backup_status.page.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:intl/intl.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; - -@RoutePage() -class FailedBackupStatusPage extends HookConsumerWidget { - const FailedBackupStatusPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final errorBackupList = ref.watch(errorBackupListProvider); - - return Scaffold( - appBar: AppBar( - elevation: 0, - title: Text( - "Failed Backup (${errorBackupList.length})", - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - leading: IconButton( - onPressed: () { - context.maybePop(true); - }, - splashRadius: 24, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - ), - body: ListView.builder( - shrinkWrap: true, - itemCount: errorBackupList.length, - itemBuilder: ((context, index) { - var errorAsset = errorBackupList.elementAt(index); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4), - child: Card( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(15), // if you need this - ), - side: BorderSide(color: Colors.black12, width: 1), - ), - elevation: 0, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100, minHeight: 100, maxWidth: 100, maxHeight: 150), - child: ClipRRect( - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(15), - topLeft: Radius.circular(15), - ), - clipBehavior: Clip.hardEdge, - child: Image( - fit: BoxFit.cover, - image: LocalThumbProvider(id: errorAsset.asset.localId!, assetType: base_asset.AssetType.video), - ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - DateFormat.yMMMMd().format( - DateTime.parse(errorAsset.fileCreatedAt.toString()).toLocal(), - ), - style: TextStyle( - fontWeight: FontWeight.w600, - color: context.isDarkTheme ? Colors.white70 : Colors.grey[800], - ), - ), - Icon(Icons.error, color: Colors.red.withAlpha(200), size: 18), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - errorAsset.fileName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), - ), - ), - Text( - errorAsset.errorMessage, - style: TextStyle( - fontWeight: FontWeight.w500, - color: context.isDarkTheme ? Colors.white70 : Colors.grey[800], - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - }), - ), - ); - } -} diff --git a/mobile/lib/pages/common/activities.page.dart b/mobile/lib/pages/common/activities.page.dart deleted file mode 100644 index 9d1123dbca..0000000000 --- a/mobile/lib/pages/common/activities.page.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; - -@RoutePage() -class ActivitiesPage extends HookConsumerWidget { - const ActivitiesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Album has to be set in the provider before reaching this page - final album = ref.watch(currentAlbumProvider)!; - final asset = ref.watch(currentAssetProvider); - final user = ref.watch(currentUserProvider); - - final activityNotifier = ref.read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier); - final activities = ref.watch(albumActivityProvider(album.remoteId!, asset?.remoteId)); - - final listViewScrollController = useScrollController(); - - Future onAddComment(String comment) async { - await activityNotifier.addComment(comment); - // Scroll to the end of the list to show the newly added activity - await listViewScrollController.animateTo( - listViewScrollController.position.maxScrollExtent + 200, - duration: const Duration(milliseconds: 600), - curve: Curves.fastOutSlowIn, - ); - } - - return Scaffold( - appBar: AppBar(title: asset == null ? Text(album.name) : null), - body: activities.widgetWhen( - onData: (data) { - final liked = data.firstWhereOrNull( - (a) => a.type == ActivityType.like && a.user.id == user?.id && a.assetId == asset?.remoteId, - ); - - return SafeArea( - child: Stack( - children: [ - ListView.builder( - controller: listViewScrollController, - // +1 to display an additional over-scroll space after the last element - itemCount: data.length + 1, - itemBuilder: (context, index) { - // Additional vertical gap after the last element - if (index == data.length) { - return const SizedBox(height: 80); - } - - final activity = data[index]; - final canDelete = activity.user.id == user?.id || album.ownerId == user?.id; - - return Padding( - padding: const EdgeInsets.all(5), - child: DismissibleActivity( - activity.id, - ActivityTile(activity), - onDismiss: canDelete - ? (activityId) async => await activityNotifier.removeActivity(activity.id) - : null, - ), - ); - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - color: context.scaffoldBackgroundColor, - child: ActivityTextField( - isEnabled: album.activityEnabled, - likeId: liked?.id, - onSubmit: onAddComment, - ), - ), - ), - ], - ), - ); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/common/change_experience.page.dart b/mobile/lib/pages/common/change_experience.page.dart deleted file mode 100644 index 2cc3dede1e..0000000000 --- a/mobile/lib/pages/common/change_experience.page.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.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/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.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/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/utils/migration.dart'; -import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; - -@RoutePage() -class ChangeExperiencePage extends ConsumerStatefulWidget { - final bool switchingToBeta; - - const ChangeExperiencePage({super.key, required this.switchingToBeta}); - - @override - ConsumerState createState() => _ChangeExperiencePageState(); -} - -class _ChangeExperiencePageState extends ConsumerState { - AsyncValue hasMigrated = const AsyncValue.loading(); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => _handleMigration()); - } - - Future _handleMigration() async { - try { - 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(() { - HapticFeedback.heavyImpact(); - hasMigrated = const AsyncValue.data(true); - }); - } - } catch (e, s) { - Logger("ChangeExperiencePage").severe("Error during migration", e, s); - if (mounted) { - setState(() { - hasMigrated = AsyncValue.error(e, s); - }); - } - } - } - - Future _performMigrationLogic() async { - if (widget.switchingToBeta) { - final assetNotifier = ref.read(assetProvider.notifier); - if (assetNotifier.mounted) { - assetNotifier.dispose(); - } - final albumNotifier = ref.read(albumProvider.notifier); - if (albumNotifier.mounted) { - albumNotifier.dispose(); - } - - // Cancel uploads - await Store.put(StoreKey.backgroundBackup, false); - ref - .read(backupProvider.notifier) - .configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {}); - ref.read(backupProvider.notifier).setAutoBackup(false); - ref.read(backupProvider.notifier).cancelBackup(); - ref.read(manualUploadProvider.notifier).cancelBackup(); - // Start listening to new websocket events - ref.read(websocketProvider.notifier).stopListenToOldEvents(); - ref.read(websocketProvider.notifier).startListeningToBetaEvents(); - - await ref.read(driftProvider).reset(); - await Store.put(StoreKey.shouldResetSync, true); - final delay = Store.get(StoreKey.backupTriggerDelay, AppSettingsEnum.backupTriggerDelay.defaultValue); - if (delay >= 1000) { - await Store.put(StoreKey.backupTriggerDelay, (delay / 1000).toInt()); - } - final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - - if (permission.isGranted) { - await ref.read(backgroundSyncProvider).syncLocal(full: true); - await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await ref.read(backgroundServiceProvider).disableService(); - } - } else { - await ref.read(backgroundSyncProvider).cancel(); - ref.read(websocketProvider.notifier).stopListeningToBetaEvents(); - ref.read(websocketProvider.notifier).startListeningToOldEvents(); - ref.read(readonlyModeProvider.notifier).setReadonlyMode(false); - await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider)); - await ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); - await ref.read(backgroundWorkerFgServiceProvider).disable(); - } - - await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - AnimatedSwitcher( - duration: Durations.long4, - child: hasMigrated.when( - data: (data) => const Icon(Icons.check_circle_rounded, color: Colors.green, size: 48.0), - error: (error, stackTrace) => const Icon(Icons.error, color: Colors.red, size: 48.0), - loading: () => const SizedBox(width: 50.0, height: 50.0, child: CircularProgressIndicator()), - ), - ), - const SizedBox(height: 16.0), - SizedBox( - width: 300.0, - child: AnimatedSwitcher( - duration: Durations.long4, - child: hasMigrated.when( - data: (data) => Text( - "Migration success!\nPlease close and reopen the app to apply changes", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - error: (error, stackTrace) => Text( - "Migration failed!\nError: $error", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - loading: () => Text( - "Data migration in progress...\nPlease wait and don't close this page", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart deleted file mode 100644 index 0a28dfeb5a..0000000000 --- a/mobile/lib/pages/common/create_album.page.dart +++ /dev/null @@ -1,238 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_title.provider.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; -import 'package:immich_mobile/widgets/album/album_title_text_field.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart'; -import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart'; - -@RoutePage() -// ignore: must_be_immutable -class CreateAlbumPage extends HookConsumerWidget { - final List? assets; - - const CreateAlbumPage({super.key, this.assets}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumTitleController = useTextEditingController.fromValue(TextEditingValue.empty); - final albumTitleTextFieldFocusNode = useFocusNode(); - final albumDescriptionTextFieldFocusNode = useFocusNode(); - final isAlbumTitleTextFieldFocus = useState(false); - final isAlbumTitleEmpty = useState(true); - final selectedAssets = useState>(assets != null ? Set.from(assets!) : const {}); - - void onBackgroundTapped() { - albumTitleTextFieldFocusNode.unfocus(); - albumDescriptionTextFieldFocusNode.unfocus(); - isAlbumTitleTextFieldFocus.value = false; - - if (albumTitleController.text.isEmpty) { - albumTitleController.text = 'create_album_page_untitled'.tr(); - isAlbumTitleEmpty.value = false; - ref.watch(albumTitleProvider.notifier).setAlbumTitle('create_album_page_untitled'.tr()); - } - } - - onSelectPhotosButtonPressed() async { - AssetSelectionPageResult? selectedAsset = await context.pushRoute( - AlbumAssetSelectionRoute(existingAssets: selectedAssets.value, canDeselect: true), - ); - if (selectedAsset == null) { - selectedAssets.value = const {}; - } else { - selectedAssets.value = selectedAsset.selectedAssets; - } - } - - buildTitleInputField() { - return Padding( - padding: const EdgeInsets.only(right: 10, left: 10), - child: AlbumTitleTextField( - isAlbumTitleEmpty: isAlbumTitleEmpty, - albumTitleTextFieldFocusNode: albumTitleTextFieldFocusNode, - albumTitleController: albumTitleController, - isAlbumTitleTextFieldFocus: isAlbumTitleTextFieldFocus, - ), - ); - } - - buildDescriptionInputField() { - return Padding( - padding: const EdgeInsets.only(right: 10, left: 10), - child: AlbumViewerEditableDescription( - albumDescription: '', - descriptionFocusNode: albumDescriptionTextFieldFocusNode, - ), - ); - } - - buildTitle() { - if (selectedAssets.value.isEmpty) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: 200, left: 18), - child: Text('create_shared_album_page_share_add_assets', style: context.textTheme.labelLarge).tr(), - ), - ); - } - - return const SliverToBoxAdapter(); - } - - buildSelectPhotosButton() { - if (selectedAssets.value.isEmpty) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: 16, left: 16, right: 16), - child: FilledButton.icon( - style: FilledButton.styleFrom( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16), - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), - backgroundColor: context.colorScheme.surfaceContainerHigh, - ), - onPressed: onSelectPhotosButtonPressed, - icon: Icon(Icons.add_rounded, color: context.primaryColor), - label: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'create_shared_album_page_share_select_photos', - style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: context.primaryColor, - ), - ).tr(), - ), - ), - ), - ); - } - - return const SliverToBoxAdapter(); - } - - buildControlButton() { - return Padding( - padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16), - child: SizedBox( - height: 42, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - AlbumActionFilledButton( - iconData: Icons.add_photo_alternate_outlined, - onPressed: onSelectPhotosButtonPressed, - labelText: "add_photos".tr(), - ), - ], - ), - ), - ); - } - - buildSelectedImageGrid() { - if (selectedAssets.value.isNotEmpty) { - return SliverPadding( - padding: const EdgeInsets.only(top: 16), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 5.0, - mainAxisSpacing: 5, - ), - delegate: SliverChildBuilderDelegate((BuildContext context, int index) { - return GestureDetector( - onTap: onBackgroundTapped, - child: SharedAlbumThumbnailImage(asset: selectedAssets.value.elementAt(index)), - ); - }, childCount: selectedAssets.value.length), - ), - ); - } - - return const SliverToBoxAdapter(); - } - - Future createAlbum() async { - onBackgroundTapped(); - var newAlbum = await ref - .watch(albumProvider.notifier) - .createAlbum(ref.read(albumTitleProvider), selectedAssets.value); - - if (newAlbum != null) { - await ref.read(albumProvider.notifier).refreshRemoteAlbums(); - selectedAssets.value = {}; - ref.read(albumTitleProvider.notifier).clearAlbumTitle(); - ref.read(albumViewerProvider.notifier).disableEditAlbum(); - unawaited(context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id))); - } - } - - return Scaffold( - appBar: AppBar( - elevation: 0, - centerTitle: false, - backgroundColor: context.scaffoldBackgroundColor, - leading: IconButton( - onPressed: () { - selectedAssets.value = {}; - context.maybePop(); - }, - icon: const Icon(Icons.close_rounded), - ), - title: const Text('create_album').tr(), - actions: [ - TextButton( - onPressed: albumTitleController.text.isNotEmpty ? createAlbum : null, - child: Text( - 'create'.tr(), - style: TextStyle( - fontWeight: FontWeight.bold, - color: albumTitleController.text.isNotEmpty ? context.primaryColor : context.themeData.disabledColor, - ), - ), - ), - ], - ), - body: GestureDetector( - onTap: onBackgroundTapped, - child: CustomScrollView( - slivers: [ - SliverAppBar( - backgroundColor: context.scaffoldBackgroundColor, - elevation: 5, - automaticallyImplyLeading: false, - pinned: true, - floating: false, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(125.0), - child: Column( - children: [ - buildTitleInputField(), - buildDescriptionInputField(), - if (selectedAssets.value.isNotEmpty) buildControlButton(), - ], - ), - ), - ), - buildTitle(), - buildSelectPhotosButton(), - buildSelectedImageGrid(), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/common/gallery_stacked_children.dart b/mobile/lib/pages/common/gallery_stacked_children.dart deleted file mode 100644 index 68123509ae..0000000000 --- a/mobile/lib/pages/common/gallery_stacked_children.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; - -class GalleryStackedChildren extends HookConsumerWidget { - final ValueNotifier stackIndex; - - const GalleryStackedChildren(this.stackIndex, {super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = ref.watch(currentAssetProvider); - if (asset == null) { - return const SizedBox(); - } - - final stackId = asset.stackId; - if (stackId == null) { - return const SizedBox(); - } - - final stackElements = ref.watch(assetStackStateProvider(stackId)); - final showControls = ref.watch(showControlsProvider); - - return IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: showControls ? 1.0 : 0.0, - child: SizedBox( - height: 80, - child: ListView.builder( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - itemCount: stackElements.length, - padding: const EdgeInsets.only(left: 5, right: 5, bottom: 30), - itemBuilder: (context, index) { - final currentAsset = stackElements.elementAt(index); - final assetId = currentAsset.remoteId; - if (assetId == null) { - return const SizedBox(); - } - - return Padding( - key: ValueKey(currentAsset.id), - padding: const EdgeInsets.only(right: 5), - child: GestureDetector( - onTap: () { - stackIndex.value = index; - ref.read(currentAssetProvider.notifier).set(currentAsset); - }, - child: Container( - width: 60, - height: 60, - decoration: index == stackIndex.value - ? const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(6)), - border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 2)), - ) - : const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(6)), - border: null, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(4)), - child: Image( - fit: BoxFit.cover, - image: RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: asset.thumbhash ?? ""), - ), - ), - ), - ), - ); - }, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart deleted file mode 100644 index 1d43bff167..0000000000 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ /dev/null @@ -1,438 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:math'; -import 'dart:ui' as ui; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/scroll_extensions.dart'; -import 'package:immich_mobile/pages/common/download_panel.dart'; -import 'package:immich_mobile/pages/common/gallery_stacked_children.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_viewer/advanced_bottom_sheet.dart'; -import 'package:immich_mobile/widgets/asset_viewer/bottom_gallery_bar.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/detail_panel.dart'; -import 'package:immich_mobile/widgets/asset_viewer/gallery_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; -import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart'; -import 'package:immich_mobile/widgets/photo_view/src/photo_view_computed_scale.dart'; -import 'package:immich_mobile/widgets/photo_view/src/photo_view_scale_state.dart'; -import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attributes.dart'; - -@RoutePage() -// ignore: must_be_immutable -/// Expects [currentAssetProvider] to be set before navigating to this page -class GalleryViewerPage extends HookConsumerWidget { - final int initialIndex; - final int heroOffset; - final bool showStack; - final RenderList renderList; - - GalleryViewerPage({ - super.key, - required this.renderList, - this.initialIndex = 0, - this.heroOffset = 0, - this.showStack = false, - }) : controller = PageController(initialPage: initialIndex); - - final PageController controller; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final totalAssets = useState(renderList.totalAssets); - final isZoomed = useState(false); - final stackIndex = useState(0); - final localPosition = useRef(null); - final currentIndex = useValueNotifier(initialIndex); - final loadAsset = renderList.loadAsset; - final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider); - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - final videoPlayerKeys = useRef>({}); - - GlobalKey getVideoPlayerKey(int id) { - videoPlayerKeys.value.putIfAbsent(id, () => GlobalKey()); - return videoPlayerKeys.value[id]!; - } - - Future precacheNextImage(int index) async { - if (!context.mounted) { - return; - } - - void onError(Object exception, StackTrace? stackTrace) { - // swallow error silently - log.severe('Error precaching next image: $exception, $stackTrace'); - } - - try { - if (index < totalAssets.value && index >= 0) { - final asset = loadAsset(index); - await precacheImage( - ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height), - context, - onError: onError, - ); - } - } catch (e) { - // swallow error silently - log.severe('Error precaching next image: $e'); - await context.maybePop(); - } - } - - useEffect(() { - if (ref.read(showControlsProvider)) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - } else { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - } - - // Delay this a bit so we can finish loading the page - Timer(const Duration(milliseconds: 400), () { - precacheNextImage(currentIndex.value + 1); - }); - - return null; - }, const []); - - useEffect(() { - final asset = loadAsset(currentIndex.value); - - if (asset.isRemote) { - ref.read(castProvider.notifier).loadMediaOld(asset, false); - } else { - if (isCasting) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (context.mounted) { - ref.read(castProvider.notifier).stop(); - context.scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 1), - content: Text( - "local_asset_cast_failed".tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), - ), - ); - } - }); - } - } - return null; - }, [ref.watch(castProvider).isCasting]); - - void showInfo() { - final asset = ref.read(currentAssetProvider); - if (asset == null) { - return; - } - showModalBottomSheet( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), - barrierColor: Colors.transparent, - isScrollControlled: true, - showDragHandle: true, - enableDrag: true, - context: context, - useSafeArea: true, - builder: (context) { - return DraggableScrollableSheet( - minChildSize: 0.5, - maxChildSize: 1, - initialChildSize: 0.75, - expand: false, - builder: (context, scrollController) { - return Padding( - padding: EdgeInsets.only(bottom: context.viewInsets.bottom), - child: ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.advancedTroubleshooting) - ? AdvancedBottomSheet(assetDetail: asset, scrollController: scrollController) - : DetailPanel(asset: asset, scrollController: scrollController), - ); - }, - ); - }, - ); - } - - void handleSwipeUpDown(DragUpdateDetails details) { - const int sensitivity = 15; - const int dxThreshold = 50; - const double ratioThreshold = 3.0; - - if (isZoomed.value) { - return; - } - - // Guard [localPosition] null - if (localPosition.value == null) { - return; - } - - // Check for delta from initial down point - final d = details.localPosition - localPosition.value!; - // If the magnitude of the dx swipe is large, we probably didn't mean to go down - if (d.dx.abs() > dxThreshold) { - return; - } - - final ratio = d.dy / max(d.dx.abs(), 1); - if (d.dy > sensitivity && ratio > ratioThreshold) { - context.maybePop(); - } else if (d.dy < -sensitivity && ratio < -ratioThreshold) { - showInfo(); - } - } - - ref.listen(showControlsProvider, (_, show) { - if (show || Platform.isIOS) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - return; - } - - // This prevents the bottom bar from "dropping" while the controls are being hidden - Timer(const Duration(milliseconds: 100), () { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - }); - }); - - PhotoViewGalleryPageOptions buildImage(Asset asset) { - return PhotoViewGalleryPageOptions( - onDragStart: (_, details, __, ___) { - localPosition.value = details.localPosition; - }, - onDragUpdate: (_, details, __) { - handleSwipeUpDown(details); - }, - onTapDown: (ctx, tapDownDetails, _) { - final tapToNavigate = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.tapToNavigate); - if (!tapToNavigate) { - ref.read(showControlsProvider.notifier).toggle(); - return; - } - - double tapX = tapDownDetails.globalPosition.dx; - double screenWidth = ctx.width; - - // We want to change images if the user taps in the leftmost or - // rightmost quarter of the screen - bool tappedLeftSide = tapX < screenWidth / 4; - bool tappedRightSide = tapX > screenWidth * (3 / 4); - - int? currentPage = controller.page?.toInt(); - int maxPage = renderList.totalAssets - 1; - - if (tappedLeftSide && currentPage != null) { - // Nested if because we don't want to fallback to show/hide controls - if (currentPage != 0) { - controller.jumpToPage(currentPage - 1); - } - } else if (tappedRightSide && currentPage != null) { - // Nested if because we don't want to fallback to show/hide controls - if (currentPage != maxPage) { - controller.jumpToPage(currentPage + 1); - } - } else { - ref.read(showControlsProvider.notifier).toggle(); - } - }, - onLongPressStart: asset.isMotionPhoto - ? (_, __, ___) { - ref.read(isPlayingMotionVideoProvider.notifier).playing = true; - } - : null, - imageProvider: ImmichImage.imageProvider(asset: asset), - heroAttributes: _getHeroAttributes(asset), - filterQuality: FilterQuality.high, - tightMode: true, - initialScale: PhotoViewComputedScale.contained * 0.99, - minScale: PhotoViewComputedScale.contained * 0.99, - errorBuilder: (context, error, stackTrace) => ImmichImage(asset, fit: BoxFit.contain), - ); - } - - PhotoViewGalleryPageOptions buildVideo(BuildContext context, Asset asset) { - return PhotoViewGalleryPageOptions.customChild( - onDragStart: (_, details, __, ___) => localPosition.value = details.localPosition, - onDragUpdate: (_, details, __) => handleSwipeUpDown(details), - heroAttributes: _getHeroAttributes(asset), - filterQuality: FilterQuality.high, - initialScale: PhotoViewComputedScale.contained * 0.99, - maxScale: 1.0, - minScale: PhotoViewComputedScale.contained * 0.99, - basePosition: Alignment.center, - child: SizedBox( - width: context.width, - height: context.height, - child: NativeVideoViewerPage( - key: getVideoPlayerKey(asset.id), - asset: asset, - image: Image( - key: ValueKey(asset), - image: ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height), - fit: BoxFit.contain, - height: context.height, - width: context.width, - alignment: Alignment.center, - ), - ), - ), - ); - } - - PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) { - var newAsset = loadAsset(index); - - final stackId = newAsset.stackId; - if (stackId != null && currentIndex.value == index) { - final stackElements = ref.read(assetStackStateProvider(newAsset.stackId!)); - if (stackIndex.value < stackElements.length) { - newAsset = stackElements.elementAt(stackIndex.value); - } - } - - if (newAsset.isImage && !isPlayingMotionVideo) { - return buildImage(newAsset); - } - return buildVideo(context, newAsset); - } - - return PopScope( - // Change immersive mode back to normal "edgeToEdge" mode - onPopInvokedWithResult: (didPop, _) => SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge), - child: Scaffold( - backgroundColor: Colors.black, - body: Stack( - children: [ - PhotoViewGallery.builder( - key: const ValueKey('gallery'), - scaleStateChangedCallback: (state) { - final asset = ref.read(currentAssetProvider); - if (asset == null) { - return; - } - - if (asset.isImage && !ref.read(isPlayingMotionVideoProvider)) { - isZoomed.value = state != PhotoViewScaleState.initial; - ref.read(showControlsProvider.notifier).show = !isZoomed.value; - } - }, - gaplessPlayback: true, - loadingBuilder: (context, event, index) { - final asset = loadAsset(index); - return ClipRect( - child: Stack( - fit: StackFit.expand, - children: [ - BackdropFilter(filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10)), - ImmichThumbnail(key: ValueKey(asset), asset: asset, fit: BoxFit.contain), - ], - ), - ); - }, - pageController: controller, - scrollPhysics: isZoomed.value - ? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in - : (Platform.isIOS - ? const FastScrollPhysics() // Use bouncing physics for iOS - : const FastClampingScrollPhysics() // Use heavy physics for Android - ), - itemCount: totalAssets.value, - scrollDirection: Axis.horizontal, - onPageChanged: (value, _) { - final next = currentIndex.value < value ? value + 1 : value - 1; - - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - - final newAsset = loadAsset(value); - - currentIndex.value = value; - stackIndex.value = 0; - - ref.read(currentAssetProvider.notifier).set(newAsset); - - // Wait for page change animation to finish, then precache the next image - Timer(const Duration(milliseconds: 400), () { - precacheNextImage(next); - }); - - context.scaffoldMessenger.hideCurrentSnackBar(); - - // send image to casting if the server has it - if (newAsset.isRemote) { - ref.read(castProvider.notifier).loadMediaOld(newAsset, false); - } else { - context.scaffoldMessenger.clearSnackBars(); - - if (isCasting) { - ref.read(castProvider.notifier).stop(); - context.scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 2), - content: Text( - "local_asset_cast_failed".tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), - ), - ); - } - } - }, - builder: buildAsset, - ), - Positioned( - top: 0, - left: 0, - right: 0, - child: GalleryAppBar(key: const ValueKey('app-bar'), showInfo: showInfo), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Column( - children: [ - GalleryStackedChildren(stackIndex), - BottomGalleryBar( - key: const ValueKey('bottom-bar'), - renderList: renderList, - totalAssets: totalAssets, - controller: controller, - showStack: showStack, - stackIndex: stackIndex, - assetIndex: currentIndex, - ), - ], - ), - ), - const DownloadPanel(), - ], - ), - ), - ); - } - - @pragma('vm:prefer-inline') - PhotoViewHeroAttributes _getHeroAttributes(Asset asset) { - return PhotoViewHeroAttributes( - tag: asset.isInDb ? asset.id + heroOffset : '${asset.remoteId}-$heroOffset', - transitionOnUserGestures: true, - ); - } -} diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart deleted file mode 100644 index b1eed29c5c..0000000000 --- a/mobile/lib/pages/common/native_video_viewer.page.dart +++ /dev/null @@ -1,282 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/widgets/asset_viewer/custom_video_player_controls.dart'; -import 'package:logging/logging.dart'; -import 'package:native_video_player/native_video_player.dart'; - -@RoutePage() -class NativeVideoViewerPage extends HookConsumerWidget { - static final log = Logger('NativeVideoViewer'); - final Asset asset; - final bool showControls; - final int playbackDelayFactor; - final Widget image; - - const NativeVideoViewerPage({ - super.key, - required this.asset, - required this.image, - this.showControls = true, - this.playbackDelayFactor = 1, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final videoId = asset.id.toString(); - final controller = useState(null); - final shouldPlayOnForeground = useRef(true); - - final currentAsset = useState(ref.read(currentAssetProvider)); - final isCurrent = currentAsset.value == asset; - - // Used to show the placeholder during hero animations for remote videos to avoid a stutter - final isVisible = useState(Platform.isIOS && asset.isLocal); - - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - final isVideoReady = useState(false); - - Future createSource() async { - if (!context.mounted) { - return null; - } - - try { - final local = asset.local; - if (local != null && asset.livePhotoVideoId == null) { - final file = await local.file; - if (file == null) { - throw Exception('No file found for the video'); - } - - final source = await VideoSource.init(path: file.path, type: VideoSourceType.file); - return source; - } - - // Use a network URL for the video player controller - final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final isOriginalVideo = ref - .read(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.loadOriginalVideo); - final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback'; - final String videoUrl = asset.livePhotoVideoId != null - ? '$serverEndpoint/assets/${asset.livePhotoVideoId}/$postfixUrl' - : '$serverEndpoint/assets/${asset.remoteId}/$postfixUrl'; - - final source = await VideoSource.init( - path: videoUrl, - type: VideoSourceType.network, - headers: ApiService.getRequestHeaders(), - ); - return source; - } catch (error) { - log.severe('Error creating video source for asset ${asset.fileName}: $error'); - return null; - } - } - - final videoSource = useMemoized>(() => createSource()); - final aspectRatio = useState(asset.aspectRatio); - useMemoized(() async { - if (!context.mounted || aspectRatio.value != null) { - return null; - } - - try { - aspectRatio.value = await ref.read(assetServiceProvider).getAspectRatio(asset); - } catch (error) { - log.severe('Error getting aspect ratio for asset ${asset.fileName}: $error'); - } - }); - - void onPlaybackReady() async { - final videoController = controller.value; - if (videoController == null || !isCurrent || !context.mounted) { - return; - } - - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - notifier.onNativePlaybackReady(); - - isVideoReady.value = true; - - try { - final autoPlayVideo = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.autoPlayVideo); - if (autoPlayVideo) { - await notifier.play(); - } - await notifier.setVolume(1); - } catch (error) { - log.severe('Error playing video: $error'); - } - } - - void onPlaybackStatusChanged() { - if (!context.mounted) return; - ref.read(videoPlayerProvider(videoId).notifier).onNativeStatusChanged(); - } - - void onPlaybackPositionChanged() { - if (!context.mounted) return; - ref.read(videoPlayerProvider(videoId).notifier).onNativePositionChanged(); - } - - void onPlaybackEnded() { - if (!context.mounted) return; - - ref.read(videoPlayerProvider(videoId).notifier).onNativePlaybackEnded(); - - final videoController = controller.value; - if (videoController?.playbackInfo?.status == PlaybackStatus.stopped && - !ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.loopVideo)) { - ref.read(isPlayingMotionVideoProvider.notifier).playing = false; - } - } - - void removeListeners(NativeVideoPlayerController controller) { - controller.onPlaybackPositionChanged.removeListener(onPlaybackPositionChanged); - controller.onPlaybackStatusChanged.removeListener(onPlaybackStatusChanged); - controller.onPlaybackReady.removeListener(onPlaybackReady); - controller.onPlaybackEnded.removeListener(onPlaybackEnded); - } - - void initController(NativeVideoPlayerController nc) async { - if (controller.value != null || !context.mounted) { - return; - } - - final source = await videoSource; - if (source == null) { - return; - } - - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - notifier.attachController(nc); - - nc.onPlaybackPositionChanged.addListener(onPlaybackPositionChanged); - nc.onPlaybackStatusChanged.addListener(onPlaybackStatusChanged); - nc.onPlaybackReady.addListener(onPlaybackReady); - nc.onPlaybackEnded.addListener(onPlaybackEnded); - - unawaited( - nc.loadVideoSource(source).catchError((error) { - log.severe('Error loading video source: $error'); - }), - ); - final loopVideo = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.loopVideo); - await notifier.setLoop(loopVideo); - - controller.value = nc; - } - - ref.listen(currentAssetProvider, (_, value) { - final playerController = controller.value; - if (playerController != null && value != asset) { - removeListeners(playerController); - } - - final curAsset = currentAsset.value; - if (curAsset == asset) { - return; - } - - final imageToVideo = curAsset != null && !curAsset.isVideo; - - // No need to delay video playback when swiping from an image to a video - if (imageToVideo && Platform.isIOS) { - currentAsset.value = value; - onPlaybackReady(); - return; - } - - // Delay the video playback to avoid a stutter in the swipe animation - Timer( - Platform.isIOS - ? Duration(milliseconds: 300 * playbackDelayFactor) - : imageToVideo - ? Duration(milliseconds: 200 * playbackDelayFactor) - : Duration(milliseconds: 400 * playbackDelayFactor), - () { - if (!context.mounted) { - return; - } - - currentAsset.value = value; - if (currentAsset.value == asset) { - onPlaybackReady(); - } - }, - ); - }); - - useEffect(() { - // If opening a remote video from a hero animation, delay visibility to avoid a stutter - final timer = isVisible.value ? null : Timer(const Duration(milliseconds: 300), () => isVisible.value = true); - - return () { - timer?.cancel(); - final playerController = controller.value; - if (playerController == null) { - return; - } - removeListeners(playerController); - playerController.stop().catchError((error) { - log.fine('Error stopping video: $error'); - }); - }; - }, const []); - - useOnAppLifecycleStateChange((_, state) async { - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) { - await notifier.play(); - } else if (state == AppLifecycleState.paused) { - final videoPlaying = await controller.value?.isPlaying(); - if (videoPlaying ?? true) { - shouldPlayOnForeground.value = true; - await notifier.pause(); - } else { - shouldPlayOnForeground.value = false; - } - } - }); - - return Stack( - children: [ - // This remains under the video to avoid flickering - // For motion videos, this is the image portion of the asset - if (!isVideoReady.value || asset.isMotionPhoto) Center(key: ValueKey(asset.id), child: image), - if (aspectRatio.value != null && !isCasting) - Visibility.maintain( - key: ValueKey(asset), - visible: isVisible.value, - child: Center( - key: ValueKey(asset), - child: AspectRatio( - key: ValueKey(asset), - aspectRatio: aspectRatio.value!, - child: isCurrent ? NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController) : null, - ), - ), - ), - if (showControls) Center(child: CustomVideoPlayerControls(videoId: videoId)), - ], - ); - } -} diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index e8f5eb2ee2..65970ee294 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -2,14 +2,11 @@ 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' hide Store; -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/routing/router.dart'; import 'package:immich_mobile/widgets/settings/advanced_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart'; 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/sync_status_and_actions.dart'; import 'package:immich_mobile/widgets/settings/free_up_space_settings.dart'; @@ -38,8 +35,7 @@ enum SettingSection { Widget get widget => switch (this) { SettingSection.advanced => const AdvancedSettings(), SettingSection.assetViewer => const AssetViewerSettings(), - SettingSection.backup => - Store.tryGet(StoreKey.betaTimeline) ?? false ? const DriftBackupSettings() : const BackupSettings(), + SettingSection.backup => const DriftBackupSettings(), SettingSection.freeUpSpace => const FreeUpSpaceSettings(), SettingSection.languages => const LanguageSettings(), SettingSection.networking => const NetworkingSettings(), @@ -74,13 +70,12 @@ class _MobileLayout extends StatelessWidget { .expand( (setting) => setting == SettingSection.beta ? [ - if (Store.isBetaTimelineEnabled) - SettingsCard( - icon: Icons.sync_outlined, - title: 'sync_status'.tr(), - subtitle: 'sync_status_subtitle'.tr(), - settingRoute: const SyncStatusRoute(), - ), + SettingsCard( + icon: Icons.sync_outlined, + title: 'sync_status'.tr(), + subtitle: 'sync_status_subtitle'.tr(), + settingRoute: const SyncStatusRoute(), + ), ] : [ SettingsCard( diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 37c6b95806..725f7f9e85 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -12,13 +12,9 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.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'; import 'package:immich_mobile/routing/router.dart'; @@ -27,6 +23,8 @@ import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/widgets/common/immich_logo.dart'; import 'package:immich_mobile/widgets/common/immich_title_text.dart'; import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher.dart' show launchUrl, LaunchMode; class BootstrapErrorWidget extends StatelessWidget { @@ -323,29 +321,27 @@ class SplashScreenPageState extends ConsumerState { wsProvider.connect(); unawaited(infoProvider.getServerInfo()); - if (Store.isBetaTimelineEnabled) { - bool syncSuccess = false; + bool syncSuccess = false; + await Future.wait([ + backgroundManager.syncLocal(full: true), + backgroundManager.syncRemote().then((success) => syncSuccess = success), + ]); + + if (syncSuccess) { await Future.wait([ - backgroundManager.syncLocal(full: true), - backgroundManager.syncRemote().then((success) => syncSuccess = success), + backgroundManager.hashAssets().then((_) { + _resumeBackup(backupProvider); + }), + _resumeBackup(backupProvider), + // TODO: Bring back when the soft freeze issue is addressed + // backgroundManager.syncCloudIds(), ]); + } else { + await backgroundManager.hashAssets(); + } - if (syncSuccess) { - await Future.wait([ - backgroundManager.hashAssets().then((_) { - _resumeBackup(backupProvider); - }), - _resumeBackup(backupProvider), - // TODO: Bring back when the soft freeze issue is addressed - // backgroundManager.syncCloudIds(), - ]); - } else { - await backgroundManager.hashAssets(); - } - - if (Store.get(StoreKey.syncAlbums, false)) { - await backgroundManager.syncLinkedAlbum(); - } + if (Store.get(StoreKey.syncAlbums, false)) { + await backgroundManager.syncLinkedAlbum(); } } catch (e) { log.severe('Failed establishing connection to the server: $e'); @@ -368,58 +364,7 @@ class SplashScreenPageState extends ConsumerState { // 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) { - bool migrate = - (await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text("New Timeline Experience"), - content: const Text( - "The old timeline has been deprecated and will be removed in an upcoming release. Would you like to switch to the new timeline now?", - ), - actions: [ - TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")), - ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")), - ], - ), - )) ?? - false; - if (migrate != true) { - migrate = - (await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text("Are you sure?"), - content: const Text( - "If you choose to remain on the old timeline, you will be automatically migrated to the new timeline in an upcoming release. Would you like to switch now?", - ), - actions: [ - TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")), - ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")), - ], - ), - )) ?? - false; - } - await Store.put(StoreKey.needBetaMigration, false); - if (migrate) { - unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)])); - return; - } - } - - unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute())); - } - - if (Store.isBetaTimelineEnabled) { - return; - } - - final hasPermission = await ref.read(galleryPermissionNotifier.notifier).hasPermission; - if (hasPermission) { - // Resume backup (if enable) then navigate - await ref.watch(backupProvider.notifier).resumeBackup(); + unawaited(context.replaceRoute(const TabShellRoute())); } } diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart deleted file mode 100644 index ef637ba1c8..0000000000 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/providers/multiselect.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/routing/router.dart'; - -@RoutePage() -class TabControllerPage extends HookConsumerWidget { - const TabControllerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isRefreshingAssets = ref.watch(assetProvider); - final isRefreshingRemoteAlbums = ref.watch(isRefreshingRemoteAlbumProvider); - final isScreenLandscape = MediaQuery.orientationOf(context) == Orientation.landscape; - - Widget buildIcon({required Widget icon, required bool isProcessing}) { - if (!isProcessing) return icon; - return Stack( - alignment: Alignment.center, - clipBehavior: Clip.none, - children: [ - icon, - Positioned( - right: -18, - child: SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(context.primaryColor), - ), - ), - ), - ], - ); - } - - void onNavigationSelected(TabsRouter router, int index) { - // On Photos page menu tapped - if (router.activeIndex == 0 && index == 0) { - scrollToTopNotifierProvider.scrollToTop(); - } - - // On Search page tapped - if (router.activeIndex == 1 && index == 1) { - ref.read(searchInputFocusProvider).requestFocus(); - } - - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - router.setActiveIndex(index); - ref.read(tabProvider.notifier).state = TabEnum.values[index]; - } - - final navigationDestinations = [ - NavigationDestination( - label: 'photos'.tr(), - icon: const Icon(Icons.photo_library_outlined), - selectedIcon: buildIcon( - isProcessing: isRefreshingAssets, - icon: Icon(Icons.photo_library, color: context.primaryColor), - ), - ), - NavigationDestination( - label: 'search'.tr(), - icon: const Icon(Icons.search_rounded), - selectedIcon: Icon(Icons.search, color: context.primaryColor), - ), - NavigationDestination( - label: 'albums'.tr(), - icon: const Icon(Icons.photo_album_outlined), - selectedIcon: buildIcon( - isProcessing: isRefreshingRemoteAlbums, - icon: Icon(Icons.photo_album_rounded, color: context.primaryColor), - ), - ), - NavigationDestination( - label: 'library'.tr(), - icon: const Icon(Icons.space_dashboard_outlined), - selectedIcon: buildIcon( - isProcessing: isRefreshingAssets, - icon: Icon(Icons.space_dashboard_rounded, color: context.primaryColor), - ), - ), - ]; - - Widget bottomNavigationBar(TabsRouter tabsRouter) { - return NavigationBar( - selectedIndex: tabsRouter.activeIndex, - onDestinationSelected: (index) => onNavigationSelected(tabsRouter, index), - destinations: navigationDestinations, - ); - } - - Widget navigationRail(TabsRouter tabsRouter) { - return NavigationRail( - destinations: navigationDestinations - .map((e) => NavigationRailDestination(icon: e.icon, label: Text(e.label), selectedIcon: e.selectedIcon)) - .toList(), - onDestinationSelected: (index) => onNavigationSelected(tabsRouter, index), - selectedIndex: tabsRouter.activeIndex, - labelType: NavigationRailLabelType.all, - groupAlignment: 0.0, - ); - } - - final multiselectEnabled = ref.watch(multiselectProvider); - return AutoTabsRouter( - routes: [const PhotosRoute(), SearchRoute(), const AlbumsRoute(), const LibraryRoute()], - duration: const Duration(milliseconds: 600), - transitionBuilder: (context, child, animation) => FadeTransition(opacity: animation, child: child), - builder: (context, child) { - final tabsRouter = AutoTabsRouter.of(context); - return PopScope( - canPop: tabsRouter.activeIndex == 0, - onPopInvokedWithResult: (didPop, _) => !didPop ? tabsRouter.setActiveIndex(0) : null, - child: Scaffold( - resizeToAvoidBottomInset: false, - body: isScreenLandscape - ? Row( - children: [ - navigationRail(tabsRouter), - const VerticalDivider(), - Expanded(child: child), - ], - ) - : child, - bottomNavigationBar: multiselectEnabled || isScreenLandscape ? null : bottomNavigationBar(tabsRouter), - ), - ); - }, - ); - } -} diff --git a/mobile/lib/pages/editing/crop.page.dart b/mobile/lib/pages/editing/crop.page.dart deleted file mode 100644 index a6a66c1358..0000000000 --- a/mobile/lib/pages/editing/crop.page.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:crop_image/crop_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/pages/editing/edit.page.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart'; - -/// A widget for cropping an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to crop an image and then navigate to the [EditImagePage] with the -/// cropped image. - -@RoutePage() -class CropImagePage extends HookWidget { - final Image image; - final Asset asset; - const CropImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final cropController = useCropController(); - final aspectRatio = useState(null); - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("crop".tr()), - leading: CloseButton(color: context.primaryColor), - actions: [ - IconButton( - icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), - onPressed: () async { - final croppedImage = await cropController.croppedImage(); - unawaited(context.pushRoute(EditImageRoute(asset: asset, image: croppedImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: SafeArea( - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - Container( - padding: const EdgeInsets.only(top: 20), - width: constraints.maxWidth * 0.9, - height: constraints.maxHeight * 0.6, - child: CropImage(controller: cropController, image: image, gridColor: Colors.white), - ), - Expanded( - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: Icon(Icons.rotate_left, color: context.themeData.iconTheme.color), - onPressed: () { - cropController.rotateLeft(); - }, - ), - IconButton( - icon: Icon(Icons.rotate_right, color: context.themeData.iconTheme.color), - onPressed: () { - cropController.rotateRight(); - }, - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: null, - label: 'Free', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 1.0, - label: '1:1', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 16.0 / 9.0, - label: '16:9', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 3.0 / 2.0, - label: '3:2', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 7.0 / 5.0, - label: '7:5', - ), - ], - ), - ], - ), - ), - ), - ), - ], - ); - }, - ), - ), - ); - } -} - -class _AspectRatioButton extends StatelessWidget { - final CropController cropController; - final ValueNotifier aspectRatio; - final double? ratio; - final String label; - - const _AspectRatioButton({ - required this.cropController, - required this.aspectRatio, - required this.ratio, - required this.label, - }); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(switch (label) { - 'Free' => Icons.crop_free_rounded, - '1:1' => Icons.crop_square_rounded, - '16:9' => Icons.crop_16_9_rounded, - '3:2' => Icons.crop_3_2_rounded, - '7:5' => Icons.crop_7_5_rounded, - _ => Icons.crop_free_rounded, - }, color: aspectRatio.value == ratio ? context.primaryColor : context.themeData.iconTheme.color), - onPressed: () { - cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9); - aspectRatio.value = ratio; - cropController.aspectRatio = ratio; - }, - ), - Text(label, style: context.textTheme.displayMedium), - ], - ); - } -} diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart deleted file mode 100644 index 2889785d0b..0000000000 --- a/mobile/lib/pages/editing/edit.page.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'dart:typed_data'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_converter.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:path/path.dart' as p; - -/// A stateless widget that provides functionality for editing an image. -/// -/// This widget allows users to edit an image provided either as an [Asset] or -/// directly as an [Image]. It ensures that exactly one of these is provided. -/// -/// It also includes a conversion method to convert an [Image] to a [Uint8List] to save the image on the user's phone -/// They automatically navigate to the [HomePage] with the edited image saved and they eventually get backed up to the server. -@immutable -@RoutePage() -class EditImagePage extends ConsumerWidget { - final Asset asset; - final Image image; - final bool isEdited; - - const EditImagePage({super.key, required this.asset, required this.image, required this.isEdited}); - - Future _saveEditedImage(BuildContext context, Asset asset, Image image, WidgetRef ref) async { - try { - final Uint8List imageData = await imageToUint8List(image); - await ref - .read(fileMediaRepositoryProvider) - .saveImage(imageData, title: "${p.withoutExtension(asset.fileName)}_edited.jpg"); - await ref.read(albumProvider.notifier).refreshDeviceAlbums(); - context.navigator.popUntil((route) => route.isFirst); - ImmichToast.show(durationInSecond: 3, context: context, msg: 'Image Saved!', gravity: ToastGravity.CENTER); - } catch (e) { - ImmichToast.show( - durationInSecond: 6, - context: context, - msg: "error_saving_image".tr(namedArgs: {'error': e.toString()}), - gravity: ToastGravity.CENTER, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: Text("edit".tr()), - backgroundColor: context.scaffoldBackgroundColor, - leading: IconButton( - icon: Icon(Icons.close_rounded, color: context.primaryColor, size: 24), - onPressed: () => context.navigator.popUntil((route) => route.isFirst), - ), - actions: [ - TextButton( - onPressed: isEdited ? () => _saveEditedImage(context, asset, image, ref) : null, - child: Text("save_to_gallery".tr(), style: TextStyle(color: isEdited ? context.primaryColor : Colors.grey)), - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9), - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(7)), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.2), - spreadRadius: 2, - blurRadius: 10, - offset: const Offset(0, 3), - ), - ], - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(7)), - child: Image(image: image.image, fit: BoxFit.contain), - ), - ), - ), - ), - bottomNavigationBar: Container( - height: 70, - margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10), - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(30)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.crop_rotate_rounded, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(CropImageRoute(asset: asset, image: image)); - }, - ), - Text("crop".tr(), style: context.textTheme.displayMedium), - ], - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.filter, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(FilterImageRoute(asset: asset, image: image)); - }, - ), - Text("filter".tr(), style: context.textTheme.displayMedium), - ], - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/editing/filter.page.dart b/mobile/lib/pages/editing/filter.page.dart deleted file mode 100644 index f8b144bb96..0000000000 --- a/mobile/lib/pages/editing/filter.page.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui; - -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:immich_mobile/constants/filters.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/routing/router.dart'; - -/// A widget for filtering an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to add filters to an image and then navigate to the [EditImagePage] with the -/// final composition.' -@RoutePage() -class FilterImagePage extends HookWidget { - final Image image; - final Asset asset; - - const FilterImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final colorFilter = useState(filters[0]); - final selectedFilterIndex = useState(0); - - Future createFilteredImage(ui.Image inputImage, ColorFilter filter) { - final completer = Completer(); - final size = Size(inputImage.width.toDouble(), inputImage.height.toDouble()); - final recorder = ui.PictureRecorder(); - final canvas = Canvas(recorder); - - final paint = Paint()..colorFilter = filter; - canvas.drawImage(inputImage, Offset.zero, paint); - - recorder.endRecording().toImage(size.width.round(), size.height.round()).then((image) { - completer.complete(image); - }); - - return completer.future; - } - - void applyFilter(ColorFilter filter, int index) { - colorFilter.value = filter; - selectedFilterIndex.value = index; - } - - Future applyFilterAndConvert(ColorFilter filter) async { - final completer = Completer(); - image.image - .resolve(ImageConfiguration.empty) - .addListener( - ImageStreamListener((ImageInfo info, bool _) { - completer.complete(info.image); - }), - ); - final uiImage = await completer.future; - - final filteredUiImage = await createFilteredImage(uiImage, filter); - final byteData = await filteredUiImage.toByteData(format: ui.ImageByteFormat.png); - final pngBytes = byteData!.buffer.asUint8List(); - - return Image.memory(pngBytes, fit: BoxFit.contain); - } - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("filter".tr()), - leading: CloseButton(color: context.primaryColor), - actions: [ - IconButton( - icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), - onPressed: () async { - final filteredImage = await applyFilterAndConvert(colorFilter.value); - unawaited(context.pushRoute(EditImageRoute(asset: asset, image: filteredImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Column( - children: [ - SizedBox( - height: context.height * 0.7, - child: Center( - child: ColorFiltered(colorFilter: colorFilter.value, child: image), - ), - ), - SizedBox( - height: 120, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: filters.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: _FilterButton( - image: image, - label: filterNames[index], - filter: filters[index], - isSelected: selectedFilterIndex.value == index, - onTap: () => applyFilter(filters[index], index), - ), - ); - }, - ), - ), - ], - ), - ); - } -} - -class _FilterButton extends StatelessWidget { - final Image image; - final String label; - final ColorFilter filter; - final bool isSelected; - final VoidCallback onTap; - - const _FilterButton({ - required this.image, - required this.label, - required this.filter, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - GestureDetector( - onTap: onTap, - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10)), - border: isSelected ? Border.all(color: context.primaryColor, width: 3) : null, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(10)), - child: ColorFiltered( - colorFilter: filter, - child: FittedBox(fit: BoxFit.cover, child: image), - ), - ), - ), - ), - const SizedBox(height: 10), - Text(label, style: context.themeData.textTheme.bodyMedium), - ], - ); - } -} diff --git a/mobile/lib/pages/library/archive.page.dart b/mobile/lib/pages/library/archive.page.dart deleted file mode 100644 index 8ca1bb9752..0000000000 --- a/mobile/lib/pages/library/archive.page.dart +++ /dev/null @@ -1,37 +0,0 @@ -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/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class ArchivePage extends HookConsumerWidget { - const ArchivePage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - AppBar buildAppBar() { - final archiveRenderList = ref.watch(archiveTimelineProvider); - final count = archiveRenderList.value?.totalAssets.toString() ?? "?"; - return AppBar( - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - centerTitle: true, - automaticallyImplyLeading: false, - title: const Text('archive_page_title').tr(namedArgs: {'count': count}), - ); - } - - return Scaffold( - appBar: ref.watch(multiselectProvider) ? null : buildAppBar(), - body: MultiselectGrid( - renderListProvider: archiveTimelineProvider, - unarchive: true, - archiveEnabled: true, - deleteEnabled: true, - editEnabled: true, - ), - ); - } -} diff --git a/mobile/lib/pages/library/favorite.page.dart b/mobile/lib/pages/library/favorite.page.dart deleted file mode 100644 index 649d7727d5..0000000000 --- a/mobile/lib/pages/library/favorite.page.dart +++ /dev/null @@ -1,34 +0,0 @@ -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/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class FavoritesPage extends HookConsumerWidget { - const FavoritesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - AppBar buildAppBar() { - return AppBar( - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - centerTitle: true, - automaticallyImplyLeading: false, - title: const Text('favorites').tr(), - ); - } - - return Scaffold( - appBar: ref.watch(multiselectProvider) ? null : buildAppBar(), - body: MultiselectGrid( - renderListProvider: favoriteTimelineProvider, - favoriteEnabled: true, - editEnabled: true, - unfavorite: true, - ), - ); - } -} diff --git a/mobile/lib/pages/library/folder/folder.page.dart b/mobile/lib/pages/library/folder/folder.page.dart index 497d3e5151..9de230d550 100644 --- a/mobile/lib/pages/library/folder/folder.page.dart +++ b/mobile/lib/pages/library/folder/folder.page.dart @@ -1,19 +1,22 @@ 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:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; import 'package:immich_mobile/models/folder/root_folder.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail_tile.widget.dart'; import 'package:immich_mobile/providers/folder.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; RecursiveFolder? _findFolderInStructure(RootFolder rootFolder, RecursiveFolder targetFolder) { @@ -136,8 +139,8 @@ class FolderContent extends HookConsumerWidget { FolderPath(currentFolder: folder!, root: root), Expanded( child: folderRenderlist.when( - data: (list) { - if (folder!.subfolders.isEmpty && list.isEmpty) { + data: (folderAssets) { + if (folder!.subfolders.isEmpty && folderAssets.isEmpty) { return Center(child: const Text("empty_folder").tr()); } @@ -164,32 +167,33 @@ class FolderContent extends HookConsumerWidget { onTap: () => context.pushRoute(FolderRoute(folder: subfolder)), ), ), - if (!list.isEmpty && list.allAssets != null && list.allAssets!.isNotEmpty) - ...list.allAssets!.map( - (asset) => LargeLeadingTile( + if (folderAssets.isNotEmpty) + ...folderAssets.mapIndexed( + (index, asset) => LargeLeadingTile( onTap: () { - ref.read(currentAssetProvider.notifier).set(asset); + AssetViewer.setAsset(ref, asset); context.pushRoute( - GalleryViewerRoute(renderList: list, initialIndex: list.allAssets!.indexOf(asset)), + AssetViewerRoute( + initialIndex: index, + timelineService: ref + .read(timelineFactoryProvider) + .fromAssets(folderAssets, TimelineOrigin.folder), + ), ); }, leading: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(15)), - child: SizedBox( - width: 80, - height: 80, - child: ThumbnailImage(asset: asset, showStorageIndicator: false), - ), + child: SizedBox(width: 80, height: 80, child: ThumbnailTile(asset)), ), title: Text( - asset.fileName, + asset.name, maxLines: 2, softWrap: false, overflow: TextOverflow.ellipsis, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), ), subtitle: Text( - "${asset.exifInfo?.fileSize != null ? formatBytes(asset.exifInfo?.fileSize ?? 0) : ""} â€ĸ ${DateFormat.yMMMd().format(asset.fileCreatedAt)}", + "${asset.exifInfo.fileSize != null ? formatBytes(asset.exifInfo.fileSize ?? 0) : ""} â€ĸ ${DateFormat.yMMMd().format(asset.createdAt)}", style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ), diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart deleted file mode 100644 index 99a534e9cf..0000000000 --- a/mobile/lib/pages/library/library.page.dart +++ /dev/null @@ -1,383 +0,0 @@ -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'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; -import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; -import 'package:immich_mobile/widgets/common/user_avatar.dart'; -import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -@RoutePage() -class LibraryPage extends ConsumerWidget { - const LibraryPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - context.locale; - final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - - return Scaffold( - appBar: const ImmichAppBar(), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: ListView( - shrinkWrap: true, - children: [ - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Row( - children: [ - ActionButton( - onPressed: () => context.pushRoute(const FavoritesRoute()), - icon: Icons.favorite_outline_rounded, - label: context.t.favorites, - ), - const SizedBox(width: 8), - ActionButton( - onPressed: () => context.pushRoute(const ArchiveRoute()), - icon: Icons.archive_outlined, - label: context.t.archived, - ), - ], - ), - ), - const SizedBox(height: 8), - Row( - children: [ - ActionButton( - onPressed: () => context.pushRoute(const SharedLinkRoute()), - icon: Icons.link_outlined, - label: context.t.shared_links, - ), - SizedBox(width: trashEnabled ? 8 : 0), - trashEnabled - ? ActionButton( - onPressed: () => context.pushRoute(const TrashRoute()), - icon: Icons.delete_outline_rounded, - label: context.t.trash, - ) - : const SizedBox.shrink(), - ], - ), - const SizedBox(height: 12), - const Wrap( - spacing: 8, - runSpacing: 8, - children: [PeopleCollectionCard(), PlacesCollectionCard(), LocalAlbumsCollectionCard()], - ), - const SizedBox(height: 12), - const QuickAccessButtons(), - const SizedBox(height: 32), - ], - ), - ), - ); - } -} - -class QuickAccessButtons extends ConsumerWidget { - const QuickAccessButtons({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final partners = ref.watch(partnerSharedWithProvider); - - return Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1), - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withAlpha(10), - context.colorScheme.primary.withAlpha(15), - context.colorScheme.primary.withAlpha(20), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: const Radius.circular(20), - topRight: const Radius.circular(20), - bottomLeft: Radius.circular(partners.isEmpty ? 20 : 0), - bottomRight: Radius.circular(partners.isEmpty ? 20 : 0), - ), - ), - leading: const Icon(Icons.folder_outlined, size: 26), - title: Text(context.t.folders, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), - onTap: () => context.pushRoute(FolderRoute()), - ), - ListTile( - leading: const Icon(Icons.lock_outline_rounded, size: 26), - title: Text( - context.t.locked_folder, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), - ), - onTap: () => context.pushRoute(const LockedRoute()), - ), - ListTile( - leading: const Icon(Icons.group_outlined, size: 26), - title: Text(context.t.partners, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), - onTap: () => context.pushRoute(const PartnerRoute()), - ), - PartnerList(partners: partners), - ], - ), - ); - } -} - -class PartnerList extends ConsumerWidget { - const PartnerList({super.key, required this.partners}); - - final List partners; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ListView.builder( - physics: const NeverScrollableScrollPhysics(), - itemCount: partners.length, - shrinkWrap: true, - itemBuilder: (context, index) { - final partner = partners[index]; - final isLastItem = index == partners.length - 1; - return ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(isLastItem ? 20 : 0), - bottomRight: Radius.circular(isLastItem ? 20 : 0), - ), - ), - contentPadding: const EdgeInsets.only(left: 12.0, right: 18.0), - leading: userAvatar(context, partner, radius: 16), - title: const Text( - "partner_list_user_photos", - style: TextStyle(fontWeight: FontWeight.w500), - ).tr(namedArgs: {'user': partner.name}), - onTap: () => context.pushRoute((PartnerDetailRoute(partner: partner))), - ); - }, - ); - } -} - -class PeopleCollectionCard extends ConsumerWidget { - const PeopleCollectionCard({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final people = ref.watch(getAllPeopleProvider); - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final widthFactor = isTablet ? 0.25 : 0.5; - final size = context.width * widthFactor - 20.0; - - return GestureDetector( - onTap: () => context.pushRoute(const PeopleCollectionRoute()), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: size, - width: size, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [context.colorScheme.primary.withAlpha(30), context.colorScheme.primary.withAlpha(25)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: people.widgetWhen( - onLoading: () => const Center(child: CircularProgressIndicator()), - onData: (people) { - return GridView.count( - crossAxisCount: 2, - padding: const EdgeInsets.all(12), - crossAxisSpacing: 8, - mainAxisSpacing: 8, - physics: const NeverScrollableScrollPhysics(), - children: people.take(4).map((person) { - return CircleAvatar(backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id))); - }).toList(), - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - context.t.people, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ); - }, - ); - } -} - -class LocalAlbumsCollectionCard extends HookConsumerWidget { - const LocalAlbumsCollectionCard({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(localAlbumsProvider); - - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final widthFactor = isTablet ? 0.25 : 0.5; - final size = context.width * widthFactor - 20.0; - - return GestureDetector( - onTap: () => context.pushRoute(const LocalAlbumsRoute()), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: size, - width: size, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [context.colorScheme.primary.withAlpha(30), context.colorScheme.primary.withAlpha(25)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: GridView.count( - crossAxisCount: 2, - padding: const EdgeInsets.all(12), - crossAxisSpacing: 8, - mainAxisSpacing: 8, - physics: const NeverScrollableScrollPhysics(), - children: albums.take(4).map((album) { - return AlbumThumbnailCard(album: album, showTitle: false); - }).toList(), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - context.t.on_this_device, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ); - }, - ); - } -} - -class PlacesCollectionCard extends StatelessWidget { - const PlacesCollectionCard({super.key}); - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final widthFactor = isTablet ? 0.25 : 0.5; - final size = context.width * widthFactor - 20.0; - - return GestureDetector( - onTap: () => context.pushRoute(PlacesCollectionRoute(currentLocation: null)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: size, - width: size, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - color: context.colorScheme.secondaryContainer.withAlpha(100), - ), - child: IgnorePointer( - child: MapThumbnail( - zoom: 8, - centre: const LatLng(21.44950, -157.91959), - showAttribution: false, - themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - context.t.places, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ); - }, - ); - } -} - -class ActionButton extends StatelessWidget { - final VoidCallback onPressed; - final IconData icon; - final String label; - - const ActionButton({super.key, required this.onPressed, required this.icon, required this.label}); - - @override - Widget build(BuildContext context) { - return Expanded( - child: FilledButton.icon( - onPressed: onPressed, - label: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text(label, style: TextStyle(color: context.colorScheme.onSurface, fontSize: 15)), - ), - style: FilledButton.styleFrom( - elevation: 0, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - backgroundColor: context.colorScheme.surfaceContainerLow, - alignment: Alignment.centerLeft, - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(25)), - side: BorderSide(color: context.colorScheme.onSurface.withAlpha(10), width: 1), - ), - ), - icon: Icon(icon, color: context.primaryColor), - ), - ); - } -} diff --git a/mobile/lib/pages/library/local_albums.page.dart b/mobile/lib/pages/library/local_albums.page.dart deleted file mode 100644 index e52a8326df..0000000000 --- a/mobile/lib/pages/library/local_albums.page.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -@RoutePage() -class LocalAlbumsPage extends HookConsumerWidget { - const LocalAlbumsPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(localAlbumsProvider); - - return Scaffold( - appBar: AppBar(title: Text('on_this_device'.tr())), - body: ListView.builder( - padding: const EdgeInsets.all(18.0), - itemCount: albums.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: LargeLeadingTile( - leadingPadding: const EdgeInsets.only(right: 16), - leading: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(15)), - child: ImmichThumbnail(asset: albums[index].thumbnail.value, width: 80, height: 80), - ), - title: Text( - albums[index].name, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), - ), - subtitle: Text( - 'items_count'.t(context: context, args: {'count': albums[index].assetCount}), - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ), - onTap: () => context.pushRoute(AlbumViewerRoute(albumId: albums[index].id)), - ), - ); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/library/locked/locked.page.dart b/mobile/lib/pages/library/locked/locked.page.dart deleted file mode 100644 index aea62e0051..0000000000 --- a/mobile/lib/pages/library/locked/locked.page.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class LockedPage extends HookConsumerWidget { - const LockedPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final appLifeCycle = useAppLifecycleState(); - final showOverlay = useState(false); - final authProviderNotifier = ref.read(authProvider.notifier); - // lock the page when it is destroyed - useEffect(() { - return () { - authProviderNotifier.lockPinCode(); - }; - }, []); - - useEffect(() { - if (context.mounted) { - if (appLifeCycle == AppLifecycleState.resumed) { - showOverlay.value = false; - } else { - showOverlay.value = true; - } - } - - return null; - }, [appLifeCycle]); - - return Scaffold( - appBar: ref.watch(multiselectProvider) ? null : const LockPageAppBar(), - body: showOverlay.value - ? const SizedBox() - : MultiselectGrid( - renderListProvider: lockedTimelineProvider, - topWidget: Padding( - padding: const EdgeInsets.all(16.0), - child: Center(child: Text('no_locked_photos_message'.tr(), style: context.textTheme.labelLarge)), - ), - editEnabled: false, - favoriteEnabled: false, - unfavorite: false, - archiveEnabled: false, - stackEnabled: false, - unarchive: false, - ), - ); - } -} - -class LockPageAppBar extends ConsumerWidget implements PreferredSizeWidget { - const LockPageAppBar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return AppBar( - leading: IconButton( - onPressed: () { - ref.read(authProvider.notifier).lockPinCode(); - context.maybePop(); - }, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - centerTitle: true, - automaticallyImplyLeading: false, - title: const Text('locked_folder').tr(), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} diff --git a/mobile/lib/pages/library/locked/pin_auth.page.dart b/mobile/lib/pages/library/locked/pin_auth.page.dart index a39c91871b..3af320dc5f 100644 --- a/mobile/lib/pages/library/locked/pin_auth.page.dart +++ b/mobile/lib/pages/library/locked/pin_auth.page.dart @@ -5,7 +5,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' show useState; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/local_auth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -22,7 +21,6 @@ class PinAuthPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final localAuthState = ref.watch(localAuthProvider); final showPinRegistrationForm = useState(createPinCode); - final isBetaTimeline = Store.isBetaTimelineEnabled; Future registerBiometric(String pinCode) async { final isRegistered = await ref.read(localAuthProvider.notifier).registerBiometric(context, pinCode); @@ -36,11 +34,7 @@ class PinAuthPage extends HookConsumerWidget { ), ); - if (isBetaTimeline) { - unawaited(context.replaceRoute(const DriftLockedFolderRoute())); - } else { - unawaited(context.replaceRoute(const LockedRoute())); - } + unawaited(context.replaceRoute(const DriftLockedFolderRoute())); } } @@ -89,11 +83,7 @@ class PinAuthPage extends HookConsumerWidget { child: PinVerificationForm( autoFocus: true, onSuccess: (_) { - if (isBetaTimeline) { - context.replaceRoute(const DriftLockedFolderRoute()); - } else { - context.replaceRoute(const LockedRoute()); - } + context.replaceRoute(const DriftLockedFolderRoute()); }, ), ), diff --git a/mobile/lib/pages/library/partner/drift_partner.page.dart b/mobile/lib/pages/library/partner/drift_partner.page.dart index d81cc44c76..a24323c02a 100644 --- a/mobile/lib/pages/library/partner/drift_partner.page.dart +++ b/mobile/lib/pages/library/partner/drift_partner.page.dart @@ -3,7 +3,6 @@ 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'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/people/partner_user_avatar.widget.dart'; import 'package:immich_mobile/providers/infrastructure/partner.provider.dart'; diff --git a/mobile/lib/pages/library/partner/partner.page.dart b/mobile/lib/pages/library/partner/partner.page.dart deleted file mode 100644 index eae4228a2d..0000000000 --- a/mobile/lib/pages/library/partner/partner.page.dart +++ /dev/null @@ -1,139 +0,0 @@ -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'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/services/partner.service.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/user_avatar.dart'; - -@RoutePage() -class PartnerPage extends HookConsumerWidget { - const PartnerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final List partners = ref.watch(partnerSharedByProvider); - final availableUsers = ref.watch(partnerAvailableProvider); - - addNewUsersHandler() async { - final users = availableUsers.value; - if (users == null || users.isEmpty) { - ImmichToast.show(context: context, msg: "partner_page_no_more_users".tr()); - return; - } - - final selectedUser = await showDialog( - context: context, - builder: (context) { - return SimpleDialog( - title: const Text("partner_page_select_partner").tr(), - children: [ - for (UserDto u in users) - SimpleDialogOption( - onPressed: () => context.pop(u), - child: Row( - children: [ - Padding(padding: const EdgeInsets.only(right: 8), child: userAvatar(context, u)), - Text(u.name), - ], - ), - ), - ], - ); - }, - ); - if (selectedUser != null) { - final ok = await ref.read(partnerServiceProvider).addPartner(selectedUser); - if (ok) { - ref.invalidate(partnerSharedByProvider); - } else { - ImmichToast.show(context: context, msg: "partner_page_partner_add_failed".tr(), toastType: ToastType.error); - } - } - } - - onDeleteUser(UserDto u) { - return showDialog( - context: context, - builder: (BuildContext context) { - return ConfirmDialog( - title: "stop_photo_sharing", - content: "partner_page_stop_sharing_content".tr(namedArgs: {'partner': u.name}), - onOk: () => ref.read(partnerServiceProvider).removePartner(u), - ); - }, - ); - } - - buildUserList(List users) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0, top: 16.0), - child: Text( - "partner_page_shared_to_title", - style: context.textTheme.titleSmall?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), - ).tr(), - ), - if (users.isNotEmpty) - ListView.builder( - shrinkWrap: true, - itemCount: users.length, - itemBuilder: ((context, index) { - return ListTile( - leading: userAvatar(context, users[index]), - title: Text(users[index].email, style: context.textTheme.bodyLarge), - trailing: IconButton( - icon: const Icon(Icons.person_remove), - onPressed: () => onDeleteUser(users[index]), - ), - ); - }), - ), - if (users.isEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: const Text("partner_page_empty_message", style: TextStyle(fontSize: 14)).tr(), - ), - Align( - alignment: Alignment.center, - child: ElevatedButton.icon( - onPressed: availableUsers.whenOrNull(data: (data) => addNewUsersHandler), - icon: const Icon(Icons.person_add), - label: const Text("add_partner").tr(), - ), - ), - ], - ), - ), - ], - ); - } - - return Scaffold( - appBar: AppBar( - title: const Text("partners").tr(), - elevation: 0, - centerTitle: false, - actions: [ - IconButton( - onPressed: availableUsers.whenOrNull(data: (data) => addNewUsersHandler), - icon: const Icon(Icons.person_add), - tooltip: "add_partner".tr(), - ), - ], - ), - body: buildUserList(partners), - ); - } -} diff --git a/mobile/lib/pages/library/partner/partner_detail.page.dart b/mobile/lib/pages/library/partner/partner_detail.page.dart deleted file mode 100644 index 1f15dab6a3..0000000000 --- a/mobile/lib/pages/library/partner/partner_detail.page.dart +++ /dev/null @@ -1,99 +0,0 @@ -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/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -@RoutePage() -class PartnerDetailPage extends HookConsumerWidget { - const PartnerDetailPage({super.key, required this.partner}); - - final UserDto partner; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final inTimeline = useState(partner.inTimeline); - bool toggleInProcess = false; - - useEffect(() { - Future.microtask(() async => {await ref.read(assetProvider.notifier).getAllAsset()}); - return null; - }, []); - - void toggleInTimeline() async { - if (toggleInProcess) return; - toggleInProcess = true; - try { - final ok = await ref - .read(partnerSharedWithProvider.notifier) - .updatePartner(partner, inTimeline: !inTimeline.value); - if (ok) { - inTimeline.value = !inTimeline.value; - final action = inTimeline.value ? "shown on" : "hidden from"; - ImmichToast.show( - context: context, - toastType: ToastType.success, - durationInSecond: 1, - msg: "${partner.name}'s assets $action your timeline", - ); - } else { - ImmichToast.show( - context: context, - toastType: ToastType.error, - durationInSecond: 1, - msg: "Failed to toggle the timeline setting", - ); - } - } finally { - toggleInProcess = false; - } - } - - return Scaffold( - appBar: ref.watch(multiselectProvider) - ? null - : AppBar(title: Text(partner.name), elevation: 0, centerTitle: false), - body: MultiselectGrid( - topWidget: Padding( - padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0), - child: Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1), - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [context.colorScheme.primary.withAlpha(10), context.colorScheme.primary.withAlpha(15)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ListTile( - title: Text( - "Show in timeline", - style: context.textTheme.titleSmall?.copyWith(color: context.colorScheme.primary), - ), - subtitle: Text( - "Show photos and videos from this user in your timeline", - style: context.textTheme.bodyMedium, - ), - trailing: Switch(value: inTimeline.value, onChanged: (_) => toggleInTimeline()), - ), - ), - ), - ), - renderListProvider: singleUserTimelineProvider(partner.id), - onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(), - deleteEnabled: false, - favoriteEnabled: false, - ), - ); - } -} diff --git a/mobile/lib/pages/library/people/people_collection.page.dart b/mobile/lib/pages/library/people/people_collection.page.dart deleted file mode 100644 index bff52df6da..0000000000 --- a/mobile/lib/pages/library/people/people_collection.page.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; - -@RoutePage() -class PeopleCollectionPage extends HookConsumerWidget { - const PeopleCollectionPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final people = ref.watch(getAllPeopleProvider); - final formFocus = useFocusNode(); - final ValueNotifier search = useState(null); - - showNameEditModel(String personId, String personName) { - return showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: personName); - }, - ); - } - - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final isPortrait = context.orientation == Orientation.portrait; - - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: search.value == null, - title: search.value != null - ? SearchField( - focusNode: formFocus, - onTapOutside: (_) => formFocus.unfocus(), - onChanged: (value) => search.value = value, - filled: true, - hintText: 'filter_people'.tr(), - autofocus: true, - ) - : Text('people'.tr()), - actions: [ - IconButton( - icon: Icon(search.value != null ? Icons.close : Icons.search), - onPressed: () { - search.value = search.value == null ? '' : null; - }, - ), - ], - ), - body: SafeArea( - child: people.when( - data: (people) { - if (search.value != null) { - people = people.where((person) { - return person.name.toLowerCase().contains(search.value!.toLowerCase()); - }).toList(); - } - return GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isTablet ? 6 : 3, - childAspectRatio: 0.85, - mainAxisSpacing: isPortrait && isTablet ? 36 : 0, - ), - padding: const EdgeInsets.symmetric(vertical: 32), - itemCount: people.length, - itemBuilder: (context, index) { - final person = people[index]; - - return Column( - children: [ - GestureDetector( - onTap: () { - context.pushRoute(PersonResultRoute(personId: person.id, personName: person.name)); - }, - child: Material( - shape: const CircleBorder(side: BorderSide.none), - elevation: 3, - child: CircleAvatar( - maxRadius: isTablet ? 120 / 2 : 96 / 2, - backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), - ), - ), - ), - const SizedBox(height: 12), - GestureDetector( - onTap: () => showNameEditModel(person.id, person.name), - child: person.name.isEmpty - ? Text( - 'add_a_name'.tr(), - style: context.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w500, - color: context.colorScheme.primary, - ), - ) - : Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text( - person.name, - overflow: TextOverflow.ellipsis, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), - ), - ), - ), - ], - ); - }, - ); - }, - error: (error, stack) => const Text("error"), - loading: () => const Center(child: CircularProgressIndicator()), - ), - ), - ); - }, - ); - } -} diff --git a/mobile/lib/pages/library/places/places_collection.page.dart b/mobile/lib/pages/library/places/places_collection.page.dart deleted file mode 100644 index a4a6f66915..0000000000 --- a/mobile/lib/pages/library/places/places_collection.page.dart +++ /dev/null @@ -1,136 +0,0 @@ -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' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; -import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; -import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -@RoutePage() -class PlacesCollectionPage extends HookConsumerWidget { - const PlacesCollectionPage({super.key, this.currentLocation}); - final LatLng? currentLocation; - @override - Widget build(BuildContext context, WidgetRef ref) { - final places = ref.watch(getAllPlacesProvider); - final formFocus = useFocusNode(); - final ValueNotifier search = useState(null); - - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: search.value == null, - title: search.value != null - ? SearchField( - autofocus: true, - filled: true, - focusNode: formFocus, - onChanged: (value) => search.value = value, - onTapOutside: (_) => formFocus.unfocus(), - hintText: 'filter_places'.tr(), - ) - : Text('places'.tr()), - actions: [ - IconButton( - icon: Icon(search.value != null ? Icons.close : Icons.search), - onPressed: () { - search.value = search.value == null ? '' : null; - }, - ), - ], - ), - body: ListView( - shrinkWrap: true, - children: [ - if (search.value == null) - Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - height: 200, - width: context.width, - child: MapThumbnail( - onTap: (_, __) => context.pushRoute(MapRoute(initialLocation: currentLocation)), - zoom: 8, - centre: currentLocation ?? const LatLng(21.44950, -157.91959), - showAttribution: false, - themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, - ), - ), - ), - places.when( - data: (places) { - if (search.value != null) { - places = places.where((place) { - return place.label.toLowerCase().contains(search.value!.toLowerCase()); - }).toList(); - } - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: places.length, - itemBuilder: (context, index) { - final place = places[index]; - - return PlaceTile(id: place.id, name: place.label); - }, - ); - }, - error: (error, stask) => Text('error_getting_places'.tr()), - loading: () => const Center(child: CircularProgressIndicator()), - ), - ], - ), - ); - } -} - -class PlaceTile extends StatelessWidget { - const PlaceTile({super.key, required this.id, required this.name}); - - final String id; - final String name; - - @override - Widget build(BuildContext context) { - final thumbnailUrl = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail'; - - void navigateToPlace() { - context.pushRoute( - SearchRoute( - prefilter: SearchFilter( - people: {}, - location: SearchLocationFilter(city: name), - camera: SearchCameraFilter(), - date: SearchDateFilter(), - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - rating: SearchRatingFilter(), - mediaType: AssetType.other, - ), - ), - ); - } - - return LargeLeadingTile( - onTap: () => navigateToPlace(), - title: Text(name, style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500)), - leading: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(20)), - child: SizedBox( - width: 80, - height: 80, - child: Thumbnail(imageProvider: RemoteImageProvider(url: thumbnailUrl)), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/library/trash.page.dart b/mobile/lib/pages/library/trash.page.dart deleted file mode 100644 index 2279998c2d..0000000000 --- a/mobile/lib/pages/library/trash.page.dart +++ /dev/null @@ -1,225 +0,0 @@ -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:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/providers/trash.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -@RoutePage() -class TrashPage extends HookConsumerWidget { - const TrashPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final trashRenderList = ref.watch(trashTimelineProvider); - final trashDays = ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays)); - final selectionEnabledHook = useState(false); - final selection = useState({}); - final processing = useProcessingOverlay(); - - void selectionListener(bool multiselect, Set selectedAssets) { - selectionEnabledHook.value = multiselect; - selection.value = selectedAssets; - } - - onEmptyTrash() async { - processing.value = true; - await ref.read(trashProvider.notifier).emptyTrash(); - processing.value = false; - selectionEnabledHook.value = false; - if (context.mounted) { - ImmichToast.show(context: context, msg: 'trash_emptied'.tr(), gravity: ToastGravity.BOTTOM); - } - } - - handleEmptyTrash() async { - await showDialog( - context: context, - builder: (context) => ConfirmDialog( - onOk: () => onEmptyTrash(), - title: "empty_trash".tr(), - ok: "ok".tr(), - content: "trash_page_empty_trash_dialog_content".tr(), - ), - ); - } - - Future onPermanentlyDelete() async { - processing.value = true; - try { - if (selection.value.isNotEmpty) { - final isRemoved = await ref.read(assetProvider.notifier).deleteAssets(selection.value, force: true); - - if (isRemoved) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: 'assets_deleted_permanently'.tr(namedArgs: {'count': "${selection.value.length}"}), - gravity: ToastGravity.BOTTOM, - ); - } - } - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - handlePermanentDelete() async { - await showDialog( - context: context, - builder: (context) => DeleteDialog(alert: "delete_dialog_alert_remote", onDelete: () => onPermanentlyDelete()), - ); - } - - Future handleRestoreAll() async { - processing.value = true; - await ref.read(trashProvider.notifier).restoreTrash(); - processing.value = false; - selectionEnabledHook.value = false; - } - - Future handleRestore() async { - processing.value = true; - try { - if (selection.value.isNotEmpty) { - final result = await ref.read(trashProvider.notifier).restoreAssets(selection.value); - - if (result && context.mounted) { - ImmichToast.show( - context: context, - msg: 'assets_restored_successfully'.tr(namedArgs: {'count': "${selection.value.length}"}), - gravity: ToastGravity.BOTTOM, - ); - } - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - String getAppBarTitle(String count) { - if (selectionEnabledHook.value) { - return selection.value.isNotEmpty ? "${selection.value.length}" : "trash_page_select_assets_btn".tr(); - } - return 'trash_page_title'.tr(namedArgs: {'count': count}); - } - - AppBar buildAppBar(String count) { - return AppBar( - leading: IconButton( - onPressed: !selectionEnabledHook.value - ? () => context.maybePop() - : () { - selectionEnabledHook.value = false; - selection.value = {}; - }, - icon: !selectionEnabledHook.value - ? const Icon(Icons.arrow_back_ios_rounded) - : const Icon(Icons.close_rounded), - ), - centerTitle: !selectionEnabledHook.value, - automaticallyImplyLeading: false, - title: Text(getAppBarTitle(count)), - actions: [ - if (!selectionEnabledHook.value) - PopupMenuButton( - itemBuilder: (context) { - return [ - PopupMenuItem(value: () => selectionEnabledHook.value = true, child: const Text('select').tr()), - PopupMenuItem(value: handleEmptyTrash, child: const Text('empty_trash').tr()), - ]; - }, - onSelected: (fn) => fn(), - ), - ], - ); - } - - Widget buildBottomBar() { - return SafeArea( - child: Align( - alignment: Alignment.bottomCenter, - child: SizedBox( - height: 64, - child: Container( - color: context.themeData.canvasColor, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - TextButton.icon( - icon: Icon(Icons.delete_forever, color: Colors.red[400]), - label: Text( - selection.value.isEmpty ? 'trash_page_delete_all'.tr() : 'delete'.tr(), - style: TextStyle(fontSize: 14, color: Colors.red[400], fontWeight: FontWeight.bold), - ), - onPressed: processing.value - ? null - : selection.value.isEmpty - ? handleEmptyTrash - : handlePermanentDelete, - ), - TextButton.icon( - icon: const Icon(Icons.history_rounded), - label: Text( - selection.value.isEmpty ? 'trash_page_restore_all'.tr() : 'restore'.tr(), - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), - ), - onPressed: processing.value - ? null - : selection.value.isEmpty - ? handleRestoreAll - : handleRestore, - ), - ], - ), - ), - ), - ), - ); - } - - return Scaffold( - appBar: trashRenderList.maybeWhen( - orElse: () => buildAppBar("?"), - data: (data) => buildAppBar(data.totalAssets.toString()), - ), - body: trashRenderList.widgetWhen( - onData: (data) => data.isEmpty - ? Center(child: Text('trash_page_no_assets'.tr())) - : Stack( - children: [ - SafeArea( - child: ImmichAssetGrid( - renderList: data, - listener: selectionListener, - selectionActive: selectionEnabledHook.value, - showMultiSelectIndicator: false, - showStack: true, - topWidget: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24), - child: const Text("trash_page_info").tr(namedArgs: {"days": "$trashDays"}), - ), - ), - ), - if (selectionEnabledHook.value) buildBottomBar(), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/onboarding/permission_onboarding.page.dart b/mobile/lib/pages/onboarding/permission_onboarding.page.dart deleted file mode 100644 index 52d4ac0125..0000000000 --- a/mobile/lib/pages/onboarding/permission_onboarding.page.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_logo.dart'; -import 'package:immich_mobile/widgets/common/immich_title_text.dart'; -import 'package:permission_handler/permission_handler.dart'; - -@RoutePage() -class PermissionOnboardingPage extends HookConsumerWidget { - const PermissionOnboardingPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final PermissionStatus permission = ref.watch(galleryPermissionNotifier); - - // Navigate to the main Tab Controller when permission is granted - void goToBackup() => context.replaceRoute(const BackupControllerRoute()); - - // When the permission is denied, we show a request permission page - buildRequestPermission() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('permission_onboarding_request', style: context.textTheme.titleMedium, textAlign: TextAlign.center).tr(), - const SizedBox(height: 18), - ElevatedButton( - onPressed: () => - ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission().then((permission) async { - if (permission.isGranted) { - // If permission is limited, we will show the limited - // permission page - goToBackup(); - } - }), - child: const Text('continue').tr(), - ), - ], - ); - } - - // When permission is granted from outside the app, this will show to - // let them continue on to the main timeline - buildPermissionGranted() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'permission_onboarding_permission_granted', - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ).tr(), - const SizedBox(height: 18), - ElevatedButton(onPressed: () => goToBackup(), child: const Text('permission_onboarding_get_started').tr()), - ], - ); - } - - // iOS 14+ has limited permission options, which let someone just share - // a few photos with the app. If someone only has limited permissions, we - // inform that Immich works best when given full permission - buildPermissionLimited() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.warning_outlined, color: Colors.yellow, size: 48), - const SizedBox(height: 8), - Text( - 'permission_onboarding_permission_limited', - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ).tr(), - const SizedBox(height: 18), - ElevatedButton( - onPressed: () => openAppSettings(), - child: const Text('permission_onboarding_go_to_settings').tr(), - ), - const SizedBox(height: 8.0), - TextButton(onPressed: () => goToBackup(), child: const Text('permission_onboarding_continue_anyway').tr()), - ], - ); - } - - buildPermissionDenied() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.warning_outlined, color: Colors.red, size: 48), - const SizedBox(height: 8), - Text( - 'permission_onboarding_permission_denied', - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ).tr(), - const SizedBox(height: 18), - ElevatedButton( - onPressed: () => openAppSettings(), - child: const Text('permission_onboarding_go_to_settings').tr(), - ), - ], - ); - } - - final Widget child = switch (permission) { - PermissionStatus.limited => buildPermissionLimited(), - PermissionStatus.denied => buildRequestPermission(), - PermissionStatus.granted || PermissionStatus.provisional => buildPermissionGranted(), - PermissionStatus.restricted || PermissionStatus.permanentlyDenied => buildPermissionDenied(), - }; - - return Scaffold( - body: SafeArea( - child: Center( - child: SizedBox( - width: 380, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const ImmichLogo(heroTag: 'logo'), - const ImmichTitleText(), - AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: Padding(padding: const EdgeInsets.all(18.0), child: child), - ), - TextButton(child: const Text('back').tr(), onPressed: () => context.maybePop()), - ], - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/photos/memory.page.dart b/mobile/lib/pages/photos/memory.page.dart deleted file mode 100644 index bd7973bc21..0000000000 --- a/mobile/lib/pages/photos/memory.page.dart +++ /dev/null @@ -1,324 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/memories/memory_bottom_info.dart'; -import 'package:immich_mobile/widgets/memories/memory_card.dart'; -import 'package:immich_mobile/widgets/memories/memory_epilogue.dart'; -import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart'; - -@RoutePage() -/// Expects [currentAssetProvider] to be set before navigating to this page -class MemoryPage extends HookConsumerWidget { - final List memories; - final int memoryIndex; - - const MemoryPage({required this.memories, required this.memoryIndex, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentMemory = useState(memories[memoryIndex]); - final currentAssetPage = useState(0); - final currentMemoryIndex = useState(memoryIndex); - final assetProgress = useState("${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"); - const bgColor = Colors.black; - final currentAsset = useState(null); - - /// The list of all of the asset page controllers - final memoryAssetPageControllers = List.generate(memories.length, (i) => usePageController()); - - /// The main vertically scrolling page controller with each list of memories - final memoryPageController = usePageController(initialPage: memoryIndex); - - useEffect(() { - // Memories is an immersive activity - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - return () { - // Clean up to normal edge to edge when we are done - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - }; - }); - - toNextMemory() { - memoryPageController.nextPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); - } - - void toPreviousMemory() { - if (currentMemoryIndex.value > 0) { - // Move to the previous memory page - memoryPageController.previousPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); - - // Wait for the next frame to ensure the page is built - SchedulerBinding.instance.addPostFrameCallback((_) { - final previousIndex = currentMemoryIndex.value - 1; - final previousMemoryController = memoryAssetPageControllers[previousIndex]; - - // Ensure the controller is attached - if (previousMemoryController.hasClients) { - previousMemoryController.jumpToPage(memories[previousIndex].assets.length - 1); - } else { - // Wait for the next frame until it is attached - SchedulerBinding.instance.addPostFrameCallback((_) { - if (previousMemoryController.hasClients) { - previousMemoryController.jumpToPage(memories[previousIndex].assets.length - 1); - } - }); - } - }); - } - } - - toNextAsset(int currentAssetIndex) { - if (currentAssetIndex + 1 < currentMemory.value.assets.length) { - // Go to the next asset - PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; - - controller.nextPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); - } else { - // Go to the next memory since we are at the end of our assets - toNextMemory(); - } - } - - toPreviousAsset(int currentAssetIndex) { - if (currentAssetIndex > 0) { - // Go to the previous asset - PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; - - controller.previousPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); - } else { - // Go to the previous memory since we are at the end of our assets - toPreviousMemory(); - } - } - - updateProgressText() { - assetProgress.value = "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"; - } - - /// Downloads and caches the image for the asset at this [currentMemory]'s index - precacheAsset(int index) async { - // Guard index out of range - if (index < 0) { - return; - } - - // Context might be removed due to popping out of Memory Lane during Scroll handling - if (!context.mounted) { - return; - } - - late Asset asset; - if (index < currentMemory.value.assets.length) { - // Uses the next asset in this current memory - asset = currentMemory.value.assets[index]; - } else { - // Precache the first asset in the next memory if available - final currentMemoryIndex = memories.indexOf(currentMemory.value); - - // Guard no memory found - if (currentMemoryIndex == -1) { - return; - } - - final nextMemoryIndex = currentMemoryIndex + 1; - // Guard no next memory - if (nextMemoryIndex >= memories.length) { - return; - } - - // Get the first asset from the next memory - asset = memories[nextMemoryIndex].assets.first; - } - - // Precache the asset - final size = MediaQuery.sizeOf(context); - await precacheImage( - ImmichImage.imageProvider(asset: asset, width: size.width, height: size.height), - context, - size: size, - ); - } - - // Precache the next page right away if we are on the first page - if (currentAssetPage.value == 0) { - Future.delayed(const Duration(milliseconds: 200)).then((_) => precacheAsset(1)); - } - - Future onAssetChanged(int otherIndex) async { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - currentAssetPage.value = otherIndex; - updateProgressText(); - - // Wait for page change animation to finish - await Future.delayed(const Duration(milliseconds: 400)); - // And then precache the next asset - await precacheAsset(otherIndex + 1); - - final asset = currentMemory.value.assets[otherIndex]; - currentAsset.value = asset; - ref.read(currentAssetProvider.notifier).set(asset); - } - - /* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called - * when the page in the **center** of the viewer changes. We want to reset currentAssetPage only when the final - * page during the end of scroll is different than the current page - */ - return NotificationListener( - onNotification: (ScrollNotification notification) { - // Calculate OverScroll manually using the number of pixels away from maxScrollExtent - // maxScrollExtend contains the sum of horizontal pixels of all assets for depth = 1 - // or sum of vertical pixels of all memories for depth = 0 - if (notification is ScrollUpdateNotification) { - final isEpiloguePage = (memoryPageController.page?.floor() ?? 0) >= memories.length; - - final offset = notification.metrics.pixels; - if (isEpiloguePage && (offset > notification.metrics.maxScrollExtent + 150)) { - context.maybePop(); - return true; - } - } - - return false; - }, - child: Scaffold( - backgroundColor: bgColor, - body: SafeArea( - child: PageView.builder( - physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - scrollDirection: Axis.vertical, - controller: memoryPageController, - onPageChanged: (pageNumber) { - ref.read(hapticFeedbackProvider.notifier).mediumImpact(); - if (pageNumber < memories.length) { - currentMemoryIndex.value = pageNumber; - currentMemory.value = memories[pageNumber]; - } - - currentAssetPage.value = 0; - - updateProgressText(); - }, - itemCount: memories.length + 1, - itemBuilder: (context, mIndex) { - // Build last page - if (mIndex == memories.length) { - return MemoryEpilogue( - onStartOver: () => memoryPageController.animateToPage( - 0, - duration: const Duration(seconds: 1), - curve: Curves.easeInOut, - ), - ); - } - // Build horizontal page - final assetController = memoryAssetPageControllers[mIndex]; - return Column( - children: [ - Padding( - padding: const EdgeInsets.only(left: 24.0, right: 24.0, top: 8.0, bottom: 2.0), - child: AnimatedBuilder( - animation: assetController, - builder: (context, child) { - double value = 0.0; - if (assetController.hasClients) { - // We can only access [page] if this has clients - value = assetController.page ?? 0; - } - return MemoryProgressIndicator( - ticks: memories[mIndex].assets.length, - value: (value + 1) / memories[mIndex].assets.length, - ); - }, - ), - ), - Expanded( - child: Stack( - children: [ - PageView.builder( - physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - controller: assetController, - onPageChanged: onAssetChanged, - scrollDirection: Axis.horizontal, - itemCount: memories[mIndex].assets.length, - itemBuilder: (context, index) { - final asset = memories[mIndex].assets[index]; - return Stack( - children: [ - Container( - color: Colors.black, - child: MemoryCard(asset: asset, title: memories[mIndex].title, showTitle: index == 0), - ), - Positioned.fill( - child: Row( - children: [ - // Left side of the screen - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - toPreviousAsset(index); - }, - ), - ), - - // Right side of the screen - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - toNextAsset(index); - }, - ), - ), - ], - ), - ), - ], - ); - }, - ), - Positioned( - top: 8, - left: 8, - child: MaterialButton( - minWidth: 0, - onPressed: () { - // auto_route doesn't invoke pop scope, so - // turn off full screen mode here - // https://github.com/Milad-Akarie/auto_route_library/issues/1799 - context.maybePop(); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - }, - shape: const CircleBorder(), - color: Colors.white.withValues(alpha: 0.2), - elevation: 0, - child: const Icon(Icons.close_rounded, color: Colors.white), - ), - ), - if (currentAsset.value != null && currentAsset.value!.isVideo) - Positioned( - bottom: 24, - right: 32, - child: Icon(Icons.videocam_outlined, color: Colors.grey[200]), - ), - ], - ), - ), - MemoryBottomInfo(memory: memories[mIndex]), - ], - ); - }, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/photos/photos.page.dart b/mobile/lib/pages/photos/photos.page.dart deleted file mode 100644 index 7f57247ec4..0000000000 --- a/mobile/lib/pages/photos/photos.page.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; -import 'package:immich_mobile/widgets/memories/memory_lane.dart'; - -@RoutePage() -class PhotosPage extends HookConsumerWidget { - const PhotosPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentUser = ref.watch(currentUserProvider); - final timelineUsers = ref.watch(timelineUsersIdsProvider); - final tipOneOpacity = useState(0.0); - final refreshCount = useState(0); - - useEffect(() { - ref.read(websocketProvider.notifier).connect(); - Future(() => ref.read(assetProvider.notifier).getAllAsset()); - Future(() => ref.read(albumProvider.notifier).refreshRemoteAlbums()); - ref.read(serverInfoProvider.notifier).getServerInfo(); - - return; - }, []); - - Widget buildLoadingIndicator() { - Timer(const Duration(seconds: 2), () => tipOneOpacity.value = 1); - - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const ImmichLoadingIndicator(), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Text( - 'home_page_building_timeline', - style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), - ).tr(), - ), - const SizedBox(height: 8), - AnimatedOpacity( - duration: const Duration(milliseconds: 1000), - opacity: tipOneOpacity.value, - child: Column( - children: [ - SizedBox( - width: 320, - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - 'home_page_first_time_notice', - textAlign: TextAlign.center, - style: context.textTheme.bodyMedium, - ).tr(), - ), - ), - ], - ), - ), - ], - ), - ); - } - - Future refreshAssets() async { - final fullRefresh = refreshCount.value > 0; - - if (fullRefresh) { - unawaited( - Future.wait([ - ref.read(assetProvider.notifier).getAllAsset(clear: true), - ref.read(albumProvider.notifier).refreshRemoteAlbums(), - ]), - ); - - // refresh was forced: user requested another refresh within 2 seconds - refreshCount.value = 0; - } else { - await ref.read(assetProvider.notifier).getAllAsset(clear: false); - - refreshCount.value++; - // set counter back to 0 if user does not request refresh again - Timer(const Duration(seconds: 4), () => refreshCount.value = 0); - } - } - - return Stack( - children: [ - MultiselectGrid( - topWidget: (currentUser != null && currentUser.memoryEnabled) ? const MemoryLane() : const SizedBox(), - renderListProvider: timelineUsers.length > 1 - ? multiUsersTimelineProvider(timelineUsers) - : singleUserTimelineProvider(currentUser?.id), - buildLoadingIndicator: buildLoadingIndicator, - onRefresh: refreshAssets, - stackEnabled: true, - archiveEnabled: true, - editEnabled: true, - ), - AnimatedPositioned( - duration: const Duration(milliseconds: 300), - top: ref.watch(multiselectProvider) ? -(kToolbarHeight + context.padding.top) : 0, - left: 0, - right: 0, - child: Container( - height: kToolbarHeight + context.padding.top, - color: context.themeData.appBarTheme.backgroundColor, - child: const ImmichAppBar(), - ), - ), - ], - ); - } -} diff --git a/mobile/lib/pages/search/all_motion_videos.page.dart b/mobile/lib/pages/search/all_motion_videos.page.dart deleted file mode 100644 index 60bb8a6cff..0000000000 --- a/mobile/lib/pages/search/all_motion_videos.page.dart +++ /dev/null @@ -1,25 +0,0 @@ -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/asyncvalue_extensions.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/providers/search/all_motion_photos.provider.dart'; - -@RoutePage() -class AllMotionPhotosPage extends HookConsumerWidget { - const AllMotionPhotosPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final motionPhotos = ref.watch(allMotionPhotosProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('search_page_motion_photos').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: motionPhotos.widgetWhen(onData: (assets) => ImmichAssetGrid(assets: assets)), - ); - } -} diff --git a/mobile/lib/pages/search/all_people.page.dart b/mobile/lib/pages/search/all_people.page.dart deleted file mode 100644 index b2814e6c13..0000000000 --- a/mobile/lib/pages/search/all_people.page.dart +++ /dev/null @@ -1,31 +0,0 @@ -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/asyncvalue_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/widgets/search/explore_grid.dart'; - -@RoutePage() -class AllPeoplePage extends HookConsumerWidget { - const AllPeoplePage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final curatedPeople = ref.watch(getAllPeopleProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('people').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: curatedPeople.widgetWhen( - onData: (people) => ExploreGrid( - isPeople: true, - curatedContent: people.map((e) => SearchCuratedContent(label: e.name, id: e.id)).toList(), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/search/all_places.page.dart b/mobile/lib/pages/search/all_places.page.dart deleted file mode 100644 index c92f87d3ac..0000000000 --- a/mobile/lib/pages/search/all_places.page.dart +++ /dev/null @@ -1,26 +0,0 @@ -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/asyncvalue_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; -import 'package:immich_mobile/widgets/search/explore_grid.dart'; - -@RoutePage() -class AllPlacesPage extends HookConsumerWidget { - const AllPlacesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - AsyncValue> places = ref.watch(getAllPlacesProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('places').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: places.widgetWhen(onData: (data) => ExploreGrid(curatedContent: data)), - ); - } -} diff --git a/mobile/lib/pages/search/all_videos.page.dart b/mobile/lib/pages/search/all_videos.page.dart deleted file mode 100644 index acad043a58..0000000000 --- a/mobile/lib/pages/search/all_videos.page.dart +++ /dev/null @@ -1,22 +0,0 @@ -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/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class AllVideosPage extends HookConsumerWidget { - const AllVideosPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: const Text('videos').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: MultiselectGrid(renderListProvider: allVideosTimelineProvider), - ); - } -} diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart deleted file mode 100644 index 993b91d8f7..0000000000 --- a/mobile/lib/pages/search/map/map.page.dart +++ /dev/null @@ -1,384 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -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:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:geolocator/geolocator.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; -import 'package:immich_mobile/models/map/map_marker.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/map/map_marker.provider.dart'; -import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/debounce.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/map_utils.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/map/map_app_bar.dart'; -import 'package:immich_mobile/widgets/map/map_asset_grid.dart'; -import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart'; -import 'package:immich_mobile/widgets/map/map_theme_override.dart'; -import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -@RoutePage() -class MapPage extends HookConsumerWidget { - const MapPage({super.key, this.initialLocation}); - final LatLng? initialLocation; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mapController = useRef(null); - final markers = useRef>([]); - final markersInBounds = useRef>([]); - final bottomSheetStreamController = useStreamController(); - final selectedMarker = useValueNotifier<_AssetMarkerMeta?>(null); - final assetsDebouncer = useDebouncer(); - final layerDebouncer = useDebouncer(interval: const Duration(seconds: 1)); - final isLoading = useProcessingOverlay(); - final scrollController = useScrollController(); - final markerDebouncer = useDebouncer(interval: const Duration(milliseconds: 800)); - final selectedAssets = useValueNotifier>({}); - const mapZoomToAssetLevel = 12.0; - - // updates the markersInBounds value with the map markers that are visible in the current - // map camera bounds - Future updateAssetsInBounds() async { - // Guard map not created - if (mapController.value == null) { - return; - } - - final bounds = await mapController.value!.getVisibleRegion(); - final inBounds = markers.value - .where((m) => bounds.contains(LatLng(m.latLng.latitude, m.latLng.longitude))) - .toList(); - // Notify bottom sheet to update asset grid only when there are new assets - if (markersInBounds.value.length != inBounds.length) { - bottomSheetStreamController.add(MapAssetsInBoundsUpdated(inBounds.map((e) => e.assetRemoteId).toList())); - } - markersInBounds.value = inBounds; - } - - // removes all sources and layers and re-adds them with the updated markers - Future reloadLayers() async { - if (mapController.value != null) { - layerDebouncer.run(() => mapController.value!.reloadAllLayersForMarkers(markers.value)); - } - } - - Future loadMarkers() async { - try { - isLoading.value = true; - markers.value = await ref.read(mapMarkersProvider.future); - assetsDebouncer.run(updateAssetsInBounds); - await reloadLayers(); - } finally { - isLoading.value = false; - } - } - - useEffect(() { - final currentAssetLink = ref.read(currentAssetProvider.notifier).ref.keepAlive(); - - loadMarkers(); - return currentAssetLink.close; - }, []); - - // Refetch markers when map state is changed - ref.listen(mapStateNotifierProvider, (_, current) { - if (current.shouldRefetchMarkers) { - markerDebouncer.run(() { - ref.invalidate(mapMarkersProvider); - // Reset marker - selectedMarker.value = null; - loadMarkers(); - ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(false); - }); - } - }); - - // updates the selected markers position based on the current map camera - Future updateAssetMarkerPosition(MapMarker marker, {bool shouldAnimate = true}) async { - final assetPoint = await mapController.value!.toScreenLocation(marker.latLng); - selectedMarker.value = _AssetMarkerMeta(point: assetPoint, marker: marker, shouldAnimate: shouldAnimate); - (assetPoint, marker, shouldAnimate); - } - - // finds the nearest asset marker from the tap point and store it as the selectedMarker - Future onMarkerClicked(Point point, LatLng _) async { - // Guard map not created - if (mapController.value == null) { - return; - } - final latlngBound = await mapController.value!.getBoundsFromPoint(point, 50); - final marker = markersInBounds.value.firstWhereOrNull( - (m) => latlngBound.contains(LatLng(m.latLng.latitude, m.latLng.longitude)), - ); - - if (marker != null) { - await updateAssetMarkerPosition(marker); - } else { - // If no asset was previously selected and no new asset is available, close the bottom sheet - if (selectedMarker.value == null) { - bottomSheetStreamController.add(const MapCloseBottomSheet()); - } - selectedMarker.value = null; - } - } - - void onMapCreated(MapLibreMapController controller) async { - mapController.value = controller; - controller.addListener(() { - if (controller.isCameraMoving && selectedMarker.value != null) { - updateAssetMarkerPosition(selectedMarker.value!.marker, shouldAnimate: false); - } - }); - } - - Future onMarkerTapped() async { - final assetId = selectedMarker.value?.marker.assetRemoteId; - if (assetId == null) { - return; - } - - final asset = await ref.read(dbProvider).assets.getByRemoteId(assetId); - if (asset == null) { - return; - } - - // Since we only have a single asset, we can just show GroupAssetBy.none - final renderList = await RenderList.fromAssets([asset], GroupAssetsBy.none); - - ref.read(currentAssetProvider.notifier).set(asset); - if (asset.isVideo) { - ref.read(showControlsProvider.notifier).show = false; - } - unawaited(context.pushRoute(GalleryViewerRoute(initialIndex: 0, heroOffset: 0, renderList: renderList))); - } - - /// BOTTOM SHEET CALLBACKS - - Future onMapMoved() async { - assetsDebouncer.run(updateAssetsInBounds); - } - - void onBottomSheetScrolled(String assetRemoteId) { - final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId); - if (assetMarker != null) { - updateAssetMarkerPosition(assetMarker); - } - } - - void onZoomToAsset(String assetRemoteId) { - final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId); - if (mapController.value != null && assetMarker != null) { - // Offset the latitude a little to show the marker just above the viewports center - final offset = context.isMobile ? 0.02 : 0; - final latlng = LatLng(assetMarker.latLng.latitude - offset, assetMarker.latLng.longitude); - mapController.value!.animateCamera( - CameraUpdate.newLatLngZoom(latlng, mapZoomToAssetLevel), - duration: const Duration(milliseconds: 800), - ); - } - } - - void onZoomToLocation() async { - final (location, error) = await MapUtils.checkPermAndGetLocation(context: context); - if (error != null) { - if (error == LocationPermission.unableToDetermine && context.mounted) { - ImmichToast.show( - context: context, - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - msg: "map_cannot_get_user_location".tr(), - ); - } - return; - } - - if (mapController.value != null && location != null) { - await mapController.value!.animateCamera( - CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), mapZoomToAssetLevel), - duration: const Duration(milliseconds: 800), - ); - } - } - - void onAssetsSelected(bool selected, Set selection) { - selectedAssets.value = selected ? selection : {}; - } - - return MapThemeOverride( - mapBuilder: (style) => context.isMobile - // Single-column - ? Scaffold( - extendBodyBehindAppBar: true, - appBar: MapAppBar(selectedAssets: selectedAssets), - body: Stack( - children: [ - _MapWithMarker( - initialLocation: initialLocation, - style: style, - selectedMarker: selectedMarker, - onMapCreated: onMapCreated, - onMapMoved: onMapMoved, - onMapClicked: onMarkerClicked, - onStyleLoaded: reloadLayers, - onMarkerTapped: onMarkerTapped, - ), - // Should be a part of the body and not scaffold::bottomsheet for the - // location button to be hit testable - MapBottomSheet( - mapEventStream: bottomSheetStreamController.stream, - onGridAssetChanged: onBottomSheetScrolled, - onZoomToAsset: onZoomToAsset, - onAssetsSelected: onAssetsSelected, - onZoomToLocation: onZoomToLocation, - selectedAssets: selectedAssets, - ), - ], - ), - ) - // Two-pane - : Row( - children: [ - Expanded( - child: Scaffold( - extendBodyBehindAppBar: true, - appBar: MapAppBar(selectedAssets: selectedAssets), - body: Stack( - children: [ - _MapWithMarker( - initialLocation: initialLocation, - style: style, - selectedMarker: selectedMarker, - onMapCreated: onMapCreated, - onMapMoved: onMapMoved, - onMapClicked: onMarkerClicked, - onStyleLoaded: reloadLayers, - onMarkerTapped: onMarkerTapped, - ), - Positioned( - right: 0, - bottom: context.padding.bottom + 16, - child: ElevatedButton( - onPressed: onZoomToLocation, - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.my_location), - ), - ), - ], - ), - ), - ), - Expanded( - child: LayoutBuilder( - builder: (ctx, constraints) => MapAssetGrid( - controller: scrollController, - mapEventStream: bottomSheetStreamController.stream, - onGridAssetChanged: onBottomSheetScrolled, - onZoomToAsset: onZoomToAsset, - onAssetsSelected: onAssetsSelected, - selectedAssets: selectedAssets, - ), - ), - ), - ], - ), - ); - } -} - -class _AssetMarkerMeta { - final Point point; - final MapMarker marker; - final bool shouldAnimate; - - const _AssetMarkerMeta({required this.point, required this.marker, required this.shouldAnimate}); - - @override - String toString() => '_AssetMarkerMeta(point: $point, marker: $marker, shouldAnimate: $shouldAnimate)'; -} - -class _MapWithMarker extends StatelessWidget { - final AsyncValue style; - final MapCreatedCallback onMapCreated; - final OnCameraIdleCallback onMapMoved; - final OnMapClickCallback onMapClicked; - final OnStyleLoadedCallback onStyleLoaded; - final Function()? onMarkerTapped; - final ValueNotifier<_AssetMarkerMeta?> selectedMarker; - final LatLng? initialLocation; - - const _MapWithMarker({ - required this.style, - required this.onMapCreated, - required this.onMapMoved, - required this.onMapClicked, - required this.onStyleLoaded, - required this.selectedMarker, - this.onMarkerTapped, - this.initialLocation, - }); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (ctx, constraints) => SizedBox( - height: constraints.maxHeight, - width: constraints.maxWidth, - child: Stack( - children: [ - style.widgetWhen( - onData: (style) => MapLibreMap( - attributionButtonMargins: const Point(8, kToolbarHeight), - initialCameraPosition: CameraPosition( - target: initialLocation ?? const LatLng(0, 0), - zoom: initialLocation != null ? 12 : 0, - ), - styleString: style, - // This is needed to update the selectedMarker's position on map camera updates - // The changes are notified through the mapController ValueListener which is added in [onMapCreated] - trackCameraPosition: true, - onMapCreated: onMapCreated, - onCameraIdle: onMapMoved, - onMapClick: onMapClicked, - onStyleLoadedCallback: onStyleLoaded, - tiltGesturesEnabled: false, - dragEnabled: false, - myLocationEnabled: false, - attributionButtonPosition: AttributionButtonPosition.topRight, - rotateGesturesEnabled: false, - ), - ), - ValueListenableBuilder( - valueListenable: selectedMarker, - builder: (ctx, value, _) => value != null - ? PositionedAssetMarkerIcon( - point: value.point, - assetRemoteId: value.marker.assetRemoteId, - assetThumbhash: '', - durationInMilliseconds: value.shouldAnimate ? 100 : 0, - onTap: onMarkerTapped, - ) - : const SizedBox.shrink(), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/search/person_result.page.dart b/mobile/lib/pages/search/person_result.page.dart deleted file mode 100644 index 8375eb14fd..0000000000 --- a/mobile/lib/pages/search/person_result.page.dart +++ /dev/null @@ -1,101 +0,0 @@ -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' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; - -@RoutePage() -class PersonResultPage extends HookConsumerWidget { - final String personId; - final String personName; - - const PersonResultPage({super.key, required this.personId, required this.personName}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final name = useState(personName); - - showEditNameDialog() { - showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: name.value); - }, - ).then((result) { - if (result != null && result.success) { - name.value = result.updatedName; - } - }); - } - - void buildBottomSheet() { - showModalBottomSheet( - backgroundColor: context.scaffoldBackgroundColor, - isScrollControlled: false, - context: context, - useSafeArea: true, - builder: (context) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.edit_outlined), - title: const Text('edit_name', style: TextStyle(fontWeight: FontWeight.bold)).tr(), - onTap: showEditNameDialog, - ), - ], - ), - ); - }, - ); - } - - buildTitleBlock() { - return GestureDetector( - onTap: showEditNameDialog, - child: name.value.isEmpty - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('add_a_name', style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor)).tr(), - Text('find_them_fast', style: context.textTheme.labelLarge).tr(), - ], - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [Text(name.value, style: context.textTheme.titleLarge, overflow: TextOverflow.ellipsis)], - ), - ); - } - - return Scaffold( - appBar: AppBar( - title: Text(name.value), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - actions: [IconButton(onPressed: buildBottomSheet, icon: const Icon(Icons.more_vert_rounded))], - ), - body: MultiselectGrid( - renderListProvider: personAssetsProvider(personId), - topWidget: Padding( - padding: const EdgeInsets.only(left: 8.0, top: 24), - child: Row( - children: [ - CircleAvatar(radius: 36, backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(personId))), - Expanded( - child: Padding(padding: const EdgeInsets.only(left: 16.0, right: 16.0), child: buildTitleBlock()), - ), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/search/recently_taken.page.dart b/mobile/lib/pages/search/recently_taken.page.dart deleted file mode 100644 index 988af2faf0..0000000000 --- a/mobile/lib/pages/search/recently_taken.page.dart +++ /dev/null @@ -1,25 +0,0 @@ -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/asyncvalue_extensions.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/providers/search/recently_taken_asset.provider.dart'; - -@RoutePage() -class RecentlyTakenPage extends HookConsumerWidget { - const RecentlyTakenPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final recents = ref.watch(recentlyTakenAssetProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('recently_taken_page_title').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: recents.widgetWhen(onData: (searchResponse) => ImmichAssetGrid(assets: searchResponse)), - ); - } -} diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart deleted file mode 100644 index dbd32ac94b..0000000000 --- a/mobile/lib/pages/search/search.page.dart +++ /dev/null @@ -1,760 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/providers/search/paginated_search.provider.dart'; -import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; -import 'package:immich_mobile/widgets/search/search_filter/camera_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/display_option_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart'; -import 'package:immich_mobile/widgets/search/search_filter/location_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart'; -import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart'; - -@RoutePage() -class SearchPage extends HookConsumerWidget { - const SearchPage({super.key, this.prefilter}); - - final SearchFilter? prefilter; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final textSearchType = useState(TextSearchType.context); - final searchHintText = useState('sunrise_on_the_beach'.tr()); - final textSearchController = useTextEditingController(); - final filter = useState( - SearchFilter( - people: prefilter?.people ?? {}, - location: prefilter?.location ?? SearchLocationFilter(), - camera: prefilter?.camera ?? SearchCameraFilter(), - date: prefilter?.date ?? SearchDateFilter(), - display: prefilter?.display ?? SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - mediaType: prefilter?.mediaType ?? AssetType.other, - rating: prefilter?.rating ?? SearchRatingFilter(), - language: "${context.locale.languageCode}-${context.locale.countryCode}", - ), - ); - - final previousFilter = useState(null); - - final peopleCurrentFilterWidget = useState(null); - final dateRangeCurrentFilterWidget = useState(null); - final cameraCurrentFilterWidget = useState(null); - final locationCurrentFilterWidget = useState(null); - final mediaTypeCurrentFilterWidget = useState(null); - final displayOptionCurrentFilterWidget = useState(null); - - final isSearching = useState(false); - - SnackBar searchInfoSnackBar(String message) { - return SnackBar( - content: Text(message, style: context.textTheme.labelLarge), - showCloseIcon: true, - behavior: SnackBarBehavior.fixed, - closeIconColor: context.colorScheme.onSurface, - ); - } - - search() async { - if (filter.value.isEmpty) { - return; - } - - if (prefilter == null && filter.value == previousFilter.value) { - return; - } - - isSearching.value = true; - ref.watch(paginatedSearchProvider.notifier).clear(); - final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value); - - if (!hasResult) { - context.showSnackBar(searchInfoSnackBar('search_no_result'.tr())); - } - - previousFilter.value = filter.value; - isSearching.value = false; - } - - loadMoreSearchResult() async { - isSearching.value = true; - final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value); - - if (!hasResult) { - context.showSnackBar(searchInfoSnackBar('search_no_more_result'.tr())); - } - - isSearching.value = false; - } - - searchPrefilter() { - if (prefilter != null) { - Future.delayed(Duration.zero, () { - search(); - - if (prefilter!.location.city != null) { - locationCurrentFilterWidget.value = Text(prefilter!.location.city!, style: context.textTheme.labelLarge); - } - }); - } - } - - useEffect(() { - Future.microtask(() => ref.invalidate(paginatedSearchProvider)); - searchPrefilter(); - - return null; - }, []); - - showPeoplePicker() { - handleOnSelect(Set value) { - filter.value = filter.value.copyWith(people: value); - - peopleCurrentFilterWidget.value = Text( - value.map((e) => e.name != '' ? e.name : 'no_name'.tr()).join(', '), - style: context.textTheme.labelLarge, - ); - } - - handleClear() { - filter.value = filter.value.copyWith(people: {}); - - peopleCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - isScrollControlled: true, - child: FractionallySizedBox( - heightFactor: 0.8, - child: FilterBottomSheetScaffold( - title: 'search_filter_people_title'.tr(), - expanded: true, - onSearch: search, - onClear: handleClear, - child: PeoplePicker(onSelect: handleOnSelect, filter: filter.value.people), - ), - ), - ); - } - - showLocationPicker() { - handleOnSelect(Map value) { - filter.value = filter.value.copyWith( - location: SearchLocationFilter(country: value['country'], city: value['city'], state: value['state']), - ); - - final locationText = []; - if (value['country'] != null) { - locationText.add(value['country']!); - } - - if (value['state'] != null) { - locationText.add(value['state']!); - } - - if (value['city'] != null) { - locationText.add(value['city']!); - } - - locationCurrentFilterWidget.value = Text(locationText.join(', '), style: context.textTheme.labelLarge); - } - - handleClear() { - filter.value = filter.value.copyWith(location: SearchLocationFilter()); - - locationCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - child: FilterBottomSheetScaffold( - title: 'search_filter_location_title'.tr(), - onSearch: search, - onClear: handleClear, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Container( - padding: EdgeInsets.only(bottom: context.viewInsets.bottom), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: LocationPicker(onSelected: handleOnSelect, filter: filter.value.location), - ), - ), - ), - ), - ); - } - - showCameraPicker() { - handleOnSelect(Map value) { - filter.value = filter.value.copyWith( - camera: SearchCameraFilter(make: value['make'], model: value['model']), - ); - - cameraCurrentFilterWidget.value = Text( - '${value['make'] ?? ''} ${value['model'] ?? ''}', - style: context.textTheme.labelLarge, - ); - } - - handleClear() { - filter.value = filter.value.copyWith(camera: SearchCameraFilter()); - - cameraCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - child: FilterBottomSheetScaffold( - title: 'search_filter_camera_title'.tr(), - onSearch: search, - onClear: handleClear, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: CameraPicker(onSelect: handleOnSelect, filter: filter.value.camera), - ), - ), - ); - } - - showDatePicker() async { - final firstDate = DateTime(1900); - final lastDate = DateTime.now(); - - final date = await showDateRangePicker( - context: context, - firstDate: firstDate, - lastDate: lastDate, - currentDate: DateTime.now(), - initialDateRange: DateTimeRange( - start: filter.value.date.takenAfter ?? lastDate, - end: filter.value.date.takenBefore ?? lastDate, - ), - helpText: 'search_filter_date_title'.tr(), - cancelText: 'cancel'.tr(), - confirmText: 'select'.tr(), - saveText: 'save'.tr(), - errorFormatText: 'invalid_date_format'.tr(), - errorInvalidText: 'invalid_date'.tr(), - fieldStartHintText: 'start_date'.tr(), - fieldEndHintText: 'end_date'.tr(), - initialEntryMode: DatePickerEntryMode.calendar, - keyboardType: TextInputType.text, - ); - - if (date == null) { - filter.value = filter.value.copyWith(date: SearchDateFilter()); - - dateRangeCurrentFilterWidget.value = null; - unawaited(search()); - return; - } - - filter.value = filter.value.copyWith( - date: SearchDateFilter( - takenAfter: date.start, - takenBefore: date.end.add(const Duration(hours: 23, minutes: 59, seconds: 59)), - ), - ); - - // If date range is less than 24 hours, set the end date to the end of the day - if (date.end.difference(date.start).inHours < 24) { - dateRangeCurrentFilterWidget.value = Text( - DateFormat.yMMMd().format(date.start.toLocal()), - style: context.textTheme.labelLarge, - ); - } else { - dateRangeCurrentFilterWidget.value = Text( - 'search_filter_date_interval'.tr( - namedArgs: { - "start": DateFormat.yMMMd().format(date.start.toLocal()), - "end": DateFormat.yMMMd().format(date.end.toLocal()), - }, - ), - style: context.textTheme.labelLarge, - ); - } - - unawaited(search()); - } - - // MEDIA PICKER - showMediaTypePicker() { - handleOnSelected(AssetType assetType) { - filter.value = filter.value.copyWith(mediaType: assetType); - - mediaTypeCurrentFilterWidget.value = Text( - assetType == AssetType.image - ? 'image'.tr() - : assetType == AssetType.video - ? 'video'.tr() - : 'all'.tr(), - style: context.textTheme.labelLarge, - ); - } - - handleClear() { - filter.value = filter.value.copyWith(mediaType: AssetType.other); - - mediaTypeCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: 'search_filter_media_type_title'.tr(), - onSearch: search, - onClear: handleClear, - child: MediaTypePicker(onSelect: handleOnSelected, filter: filter.value.mediaType), - ), - ); - } - - // DISPLAY OPTION - showDisplayOptionPicker() { - handleOnSelect(Map value) { - final filterText = []; - value.forEach((key, value) { - switch (key) { - case DisplayOption.notInAlbum: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isNotInAlbum: value)); - if (value) { - filterText.add('search_filter_display_option_not_in_album'.tr()); - } - break; - case DisplayOption.archive: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isArchive: value)); - if (value) { - filterText.add('archive'.tr()); - } - break; - case DisplayOption.favorite: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isFavorite: value)); - if (value) { - filterText.add('favorite'.tr()); - } - break; - } - }); - - if (filterText.isEmpty) { - displayOptionCurrentFilterWidget.value = null; - return; - } - - displayOptionCurrentFilterWidget.value = Text(filterText.join(', '), style: context.textTheme.labelLarge); - } - - handleClear() { - filter.value = filter.value.copyWith( - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - ); - - displayOptionCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: 'display_options'.tr(), - onSearch: search, - onClear: handleClear, - child: DisplayOptionPicker(onSelect: handleOnSelect, filter: filter.value.display), - ), - ); - } - - handleTextSubmitted(String value) { - switch (textSearchType.value) { - case TextSearchType.context: - filter.value = filter.value.copyWith(filename: '', context: value, description: '', ocr: ''); - - break; - case TextSearchType.filename: - filter.value = filter.value.copyWith(filename: value, context: '', description: '', ocr: ''); - - break; - case TextSearchType.description: - filter.value = filter.value.copyWith(filename: '', context: '', description: value, ocr: ''); - break; - case TextSearchType.ocr: - filter.value = filter.value.copyWith(filename: '', context: '', description: '', ocr: value); - break; - } - - search(); - } - - IconData getSearchPrefixIcon() => switch (textSearchType.value) { - TextSearchType.context => Icons.image_search_rounded, - TextSearchType.filename => Icons.abc_rounded, - TextSearchType.description => Icons.text_snippet_outlined, - TextSearchType.ocr => Icons.document_scanner_outlined, - }; - - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - automaticallyImplyLeading: true, - actions: [ - Padding( - padding: const EdgeInsets.only(right: 16.0), - child: MenuAnchor( - style: MenuStyle( - elevation: const WidgetStatePropertyAll(1), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - ), - padding: const WidgetStatePropertyAll(EdgeInsets.all(4)), - ), - builder: (BuildContext context, MenuController controller, Widget? child) { - return IconButton( - onPressed: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - icon: const Icon(Icons.more_vert_rounded), - tooltip: 'show_text_search_menu'.tr(), - ); - }, - menuChildren: [ - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.image_search_rounded), - title: Text( - 'search_by_context'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.context ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.context, - ), - onPressed: () { - textSearchType.value = TextSearchType.context; - searchHintText.value = 'sunrise_on_the_beach'.tr(); - }, - ), - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.abc_rounded), - title: Text( - 'search_filter_filename'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.filename ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.filename, - ), - onPressed: () { - textSearchType.value = TextSearchType.filename; - searchHintText.value = 'file_name_or_extension'.tr(); - }, - ), - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.text_snippet_outlined), - title: Text( - 'search_by_description'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.description ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.description, - ), - onPressed: () { - textSearchType.value = TextSearchType.description; - searchHintText.value = 'search_by_description_example'.tr(); - }, - ), - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.document_scanner_outlined), - title: Text( - 'search_filter_ocr'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.ocr ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.ocr, - ), - onPressed: () { - textSearchType.value = TextSearchType.ocr; - searchHintText.value = 'search_by_ocr_example'.tr(); - }, - ), - ], - ), - ), - ], - title: Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0), - borderRadius: const BorderRadius.all(Radius.circular(24)), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withValues(alpha: 0.075), - context.colorScheme.primary.withValues(alpha: 0.09), - context.colorScheme.primary.withValues(alpha: 0.075), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: SearchField( - hintText: searchHintText.value, - key: const Key('search_text_field'), - controller: textSearchController, - contentPadding: prefilter != null ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(8), - prefixIcon: prefilter != null ? null : Icon(getSearchPrefixIcon(), color: context.colorScheme.primary), - onSubmitted: handleTextSubmitted, - focusNode: ref.watch(searchInputFocusProvider), - ), - ), - ), - body: Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 12.0), - child: SizedBox( - height: 50, - child: ListView( - key: const Key('search_filter_chip_list'), - shrinkWrap: true, - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16), - children: [ - SearchFilterChip( - icon: Icons.people_alt_outlined, - onTap: showPeoplePicker, - label: 'people'.tr(), - currentFilter: peopleCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.location_on_outlined, - onTap: showLocationPicker, - label: 'search_filter_location'.tr(), - currentFilter: locationCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.camera_alt_outlined, - onTap: showCameraPicker, - label: 'camera'.tr(), - currentFilter: cameraCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.date_range_outlined, - onTap: showDatePicker, - label: 'search_filter_date'.tr(), - currentFilter: dateRangeCurrentFilterWidget.value, - ), - SearchFilterChip( - key: const Key('media_type_chip'), - icon: Icons.video_collection_outlined, - onTap: showMediaTypePicker, - label: 'search_filter_media_type'.tr(), - currentFilter: mediaTypeCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.display_settings_outlined, - onTap: showDisplayOptionPicker, - label: 'search_filter_display_options'.tr(), - currentFilter: displayOptionCurrentFilterWidget.value, - ), - ], - ), - ), - ), - if (isSearching.value) - const Expanded(child: Center(child: CircularProgressIndicator())) - else - SearchResultGrid(onScrollEnd: loadMoreSearchResult, isSearching: isSearching.value), - ], - ), - ); - } -} - -class SearchResultGrid extends StatelessWidget { - final VoidCallback onScrollEnd; - final bool isSearching; - - const SearchResultGrid({super.key, required this.onScrollEnd, this.isSearching = false}); - - @override - Widget build(BuildContext context) { - return Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: NotificationListener( - onNotification: (notification) { - final isBottomSheetNotification = - notification.context?.findAncestorWidgetOfExactType() != null; - - final metrics = notification.metrics; - final isVerticalScroll = metrics.axis == Axis.vertical; - - if (metrics.pixels >= metrics.maxScrollExtent && isVerticalScroll && !isBottomSheetNotification) { - onScrollEnd(); - } - - return true; - }, - child: MultiselectGrid( - renderListProvider: paginatedSearchRenderListProvider, - archiveEnabled: true, - deleteEnabled: true, - editEnabled: true, - favoriteEnabled: true, - stackEnabled: false, - dragScrollLabelEnabled: false, - emptyIndicator: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: !isSearching ? const SearchEmptyContent() : const SizedBox.shrink(), - ), - ), - ), - ), - ); - } -} - -class SearchEmptyContent extends StatelessWidget { - const SearchEmptyContent({super.key}); - - @override - Widget build(BuildContext context) { - return NotificationListener( - onNotification: (_) => true, - child: ListView( - shrinkWrap: false, - children: [ - const SizedBox(height: 40), - Center( - child: Image.asset( - context.isDarkTheme ? 'assets/polaroid-dark.png' : 'assets/polaroid-light.png', - height: 125, - ), - ), - const SizedBox(height: 16), - Center(child: Text('search_page_search_photos_videos'.tr(), style: context.textTheme.labelLarge)), - const SizedBox(height: 32), - const QuickLinkList(), - ], - ), - ); - } -} - -class QuickLinkList extends StatelessWidget { - const QuickLinkList({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - border: Border.all(color: context.colorScheme.outline.withAlpha(10), width: 1), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withAlpha(10), - context.colorScheme.primary.withAlpha(15), - context.colorScheme.primary.withAlpha(20), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - QuickLink( - title: 'recently_taken'.tr(), - icon: Icons.schedule_outlined, - isTop: true, - onTap: () => context.pushRoute(const RecentlyTakenRoute()), - ), - QuickLink( - title: 'videos'.tr(), - icon: Icons.play_circle_outline_rounded, - onTap: () => context.pushRoute(const AllVideosRoute()), - ), - QuickLink( - title: 'favorites'.tr(), - icon: Icons.favorite_border_rounded, - isBottom: true, - onTap: () => context.pushRoute(const FavoritesRoute()), - ), - ], - ), - ); - } -} - -class QuickLink extends StatelessWidget { - final String title; - final IconData icon; - final VoidCallback onTap; - final bool isTop; - final bool isBottom; - - const QuickLink({ - super.key, - required this.title, - required this.icon, - required this.onTap, - this.isTop = false, - this.isBottom = false, - }); - - @override - Widget build(BuildContext context) { - final borderRadius = BorderRadius.only( - topLeft: Radius.circular(isTop ? 20 : 0), - topRight: Radius.circular(isTop ? 20 : 0), - bottomLeft: Radius.circular(isBottom ? 20 : 0), - bottomRight: Radius.circular(isBottom ? 20 : 0), - ); - - return ListTile( - shape: RoundedRectangleBorder(borderRadius: borderRadius), - leading: Icon(icon, size: 26), - title: Text(title, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), - onTap: onTap, - ); - } -} diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart index 2be51fbfc9..2744b187de 100644 --- a/mobile/lib/pages/share_intent/share_intent.page.dart +++ b/mobile/lib/pages/share_intent/share_intent.page.dart @@ -2,7 +2,6 @@ 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/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; @@ -66,7 +65,7 @@ class ShareIntentPage extends ConsumerWidget { ), leading: IconButton( onPressed: () { - context.navigateTo(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()); + context.navigateTo(const TabShellRoute()); }, icon: const Icon(Icons.arrow_back), ), diff --git a/mobile/lib/platform/background_worker_api.g.dart b/mobile/lib/platform/background_worker_api.g.dart index e8c87aa1a4..580531b0f0 100644 --- a/mobile/lib/platform/background_worker_api.g.dart +++ b/mobile/lib/platform/background_worker_api.g.dart @@ -1,18 +1,29 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), 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 +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'dart:typed_data' show Float64List, Int32List, Int64List; -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); +Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; } List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { @@ -26,19 +37,65 @@ List wrapResponse({Object? result, PlatformException? error, bool empty } bool _deepEquals(Object? a, Object? b) { + if (identical(a, b)) { + return true; + } + if (a is double && b is double) { + if (a.isNaN && b.isNaN) { + return true; + } + return a == 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]), - ); + if (a.length != b.length) { + return false; + } + for (final MapEntry entryA in a.entries) { + bool found = false; + for (final MapEntry entryB in b.entries) { + if (_deepEquals(entryA.key, entryB.key)) { + if (_deepEquals(entryA.value, entryB.value)) { + found = true; + break; + } else { + return false; + } + } + } + if (!found) { + return false; + } + } + return true; } return a == b; } +int _deepHash(Object? value) { + if (value is List) { + return Object.hashAll(value.map(_deepHash)); + } + if (value is Map) { + int result = 0; + for (final MapEntry entry in value.entries) { + result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); + } + return result; + } + if (value is double && value.isNaN) { + // Normalize NaN to a consistent hash. + return 0x7FF8000000000000.hashCode; + } + if (value is double && value == 0.0) { + // Normalize -0.0 to 0.0 so they have the same hash code. + return 0.0.hashCode; + } + return value.hashCode; +} + class BackgroundWorkerSettings { BackgroundWorkerSettings({required this.requiresCharging, required this.minimumDelaySeconds}); @@ -68,12 +125,13 @@ class BackgroundWorkerSettings { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(requiresCharging, other.requiresCharging) && + _deepEquals(minimumDelaySeconds, other.minimumDelaySeconds); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class _PigeonCodec extends StandardMessageCodec { @@ -116,95 +174,59 @@ class BackgroundWorkerFgHostApi { final String pigeonVar_messageChannelSuffix; Future enable() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future saveNotificationMessage(String title, String body) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.saveNotificationMessage$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([title, body]); - 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future configure(BackgroundWorkerSettings settings) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([settings]); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future disable() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } } @@ -222,49 +244,31 @@ class BackgroundWorkerBgHostApi { final String pigeonVar_messageChannelSuffix; Future onInitialized() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.onInitialized$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future close() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } } @@ -284,7 +288,7 @@ abstract class BackgroundWorkerFlutterApi { }) { messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$messageChannelSuffix', pigeonChannelCodec, binaryMessenger: binaryMessenger, @@ -293,19 +297,11 @@ abstract class BackgroundWorkerFlutterApi { pigeonVar_channel.setMessageHandler(null); } else { pigeonVar_channel.setMessageHandler((Object? message) async { - assert( - message != null, - 'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload was null.', - ); - final List args = (message as List?)!; - final bool? arg_isRefresh = (args[0] as bool?); - assert( - arg_isRefresh != null, - 'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload was null, expected non-null bool.', - ); - final int? arg_maxSeconds = (args[1] as int?); + final List args = message! as List; + final bool arg_isRefresh = args[0]! as bool; + final int? arg_maxSeconds = args[1] as int?; try { - await api.onIosUpload(arg_isRefresh!, arg_maxSeconds); + await api.onIosUpload(arg_isRefresh, arg_maxSeconds); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -318,7 +314,7 @@ abstract class BackgroundWorkerFlutterApi { } } { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$messageChannelSuffix', pigeonChannelCodec, binaryMessenger: binaryMessenger, @@ -341,7 +337,7 @@ abstract class BackgroundWorkerFlutterApi { } } { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.cancel$messageChannelSuffix', pigeonChannelCodec, binaryMessenger: binaryMessenger, diff --git a/mobile/lib/platform/background_worker_lock_api.g.dart b/mobile/lib/platform/background_worker_lock_api.g.dart index 93852d2564..c7836c4c69 100644 --- a/mobile/lib/platform/background_worker_lock_api.g.dart +++ b/mobile/lib/platform/background_worker_lock_api.g.dart @@ -1,18 +1,29 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), 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 +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'dart:typed_data' show Float64List, Int32List, Int64List; -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); +Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; } class _PigeonCodec extends StandardMessageCodec { @@ -50,48 +61,30 @@ class BackgroundWorkerLockApi { final String pigeonVar_messageChannelSuffix; Future lock() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.lock$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future unlock() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.unlock$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } } diff --git a/mobile/lib/platform/connectivity_api.g.dart b/mobile/lib/platform/connectivity_api.g.dart index 0422d87438..8cf8979532 100644 --- a/mobile/lib/platform/connectivity_api.g.dart +++ b/mobile/lib/platform/connectivity_api.g.dart @@ -1,18 +1,29 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), 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 +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'dart:typed_data' show Float64List, Int32List, Int64List; -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); +Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; } enum NetworkCapability { cellular, wifi, vpn, unmetered } @@ -36,7 +47,7 @@ class _PigeonCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 129: - final int? value = readValue(buffer) as int?; + final value = readValue(buffer) as int?; return value == null ? null : NetworkCapability.values[value]; default: return super.readValueOfType(type, buffer); @@ -58,30 +69,21 @@ class ConnectivityApi { final String pigeonVar_messageChannelSuffix; Future> getCapabilities() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final 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(); - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return (pigeonVar_replyValue! as List).cast(); } } diff --git a/mobile/lib/platform/local_image_api.g.dart b/mobile/lib/platform/local_image_api.g.dart index f23cb86ced..fbd0876735 100644 --- a/mobile/lib/platform/local_image_api.g.dart +++ b/mobile/lib/platform/local_image_api.g.dart @@ -1,18 +1,29 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), 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 +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'dart:typed_data' show Float64List, Int32List, Int64List; -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); +Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; } class _PigeonCodec extends StandardMessageCodec { @@ -57,9 +68,9 @@ class LocalImageApi { required bool isVideo, required bool preferEncoded, }) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.LocalImageApi.requestImage$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, @@ -72,68 +83,46 @@ class LocalImageApi { isVideo, preferEncoded, ]); - 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 (pigeonVar_replyList[0] as Map?)?.cast(); - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + return (pigeonVar_replyValue as Map?)?.cast(); } Future cancelRequest(int requestId) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.LocalImageApi.cancelRequest$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([requestId]); - 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future> getThumbhash(String thumbhash) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.LocalImageApi.getThumbhash$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([thumbhash]); - 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 Map?)!.cast(); - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return (pigeonVar_replyValue! as Map).cast(); } } diff --git a/mobile/lib/platform/native_sync_api.g.dart b/mobile/lib/platform/native_sync_api.g.dart index 6681912c2f..0de86f99a0 100644 --- a/mobile/lib/platform/native_sync_api.g.dart +++ b/mobile/lib/platform/native_sync_api.g.dart @@ -1,34 +1,91 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), 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 +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'dart:typed_data' show Float64List, Int32List, Int64List; -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); +Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; } bool _deepEquals(Object? a, Object? b) { + if (identical(a, b)) { + return true; + } + if (a is double && b is double) { + if (a.isNaN && b.isNaN) { + return true; + } + return a == 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]), - ); + if (a.length != b.length) { + return false; + } + for (final MapEntry entryA in a.entries) { + bool found = false; + for (final MapEntry entryB in b.entries) { + if (_deepEquals(entryA.key, entryB.key)) { + if (_deepEquals(entryA.value, entryB.value)) { + found = true; + break; + } else { + return false; + } + } + } + if (!found) { + return false; + } + } + return true; } return a == b; } +int _deepHash(Object? value) { + if (value is List) { + return Object.hashAll(value.map(_deepHash)); + } + if (value is Map) { + int result = 0; + for (final MapEntry entry in value.entries) { + result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); + } + return result; + } + if (value is double && value.isNaN) { + // Normalize NaN to a consistent hash. + return 0x7FF8000000000000.hashCode; + } + if (value is double && value == 0.0) { + // Normalize -0.0 to 0.0 so they have the same hash code. + return 0.0.hashCode; + } + return value.hashCode; +} + enum PlatformAssetPlaybackStyle { unknown, image, video, imageAnimated, livePhoto, videoLooping } class PlatformAsset { @@ -129,12 +186,25 @@ class PlatformAsset { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(id, other.id) && + _deepEquals(name, other.name) && + _deepEquals(type, other.type) && + _deepEquals(createdAt, other.createdAt) && + _deepEquals(updatedAt, other.updatedAt) && + _deepEquals(width, other.width) && + _deepEquals(height, other.height) && + _deepEquals(durationInSeconds, other.durationInSeconds) && + _deepEquals(orientation, other.orientation) && + _deepEquals(isFavorite, other.isFavorite) && + _deepEquals(adjustmentTime, other.adjustmentTime) && + _deepEquals(latitude, other.latitude) && + _deepEquals(longitude, other.longitude) && + _deepEquals(playbackStyle, other.playbackStyle); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class PlatformAlbum { @@ -184,12 +254,16 @@ class PlatformAlbum { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(id, other.id) && + _deepEquals(name, other.name) && + _deepEquals(updatedAt, other.updatedAt) && + _deepEquals(isCloud, other.isCloud) && + _deepEquals(assetCount, other.assetCount); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class SyncDelta { @@ -215,9 +289,9 @@ class SyncDelta { result as List; return SyncDelta( hasChanges: result[0]! as bool, - updates: (result[1] as List?)!.cast(), - deletes: (result[2] as List?)!.cast(), - assetAlbums: (result[3] as Map?)!.cast>(), + updates: (result[1]! as List).cast(), + deletes: (result[2]! as List).cast(), + assetAlbums: (result[3]! as Map).cast>(), ); } @@ -230,12 +304,15 @@ class SyncDelta { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(hasChanges, other.hasChanges) && + _deepEquals(updates, other.updates) && + _deepEquals(deletes, other.deletes) && + _deepEquals(assetAlbums, other.assetAlbums); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class HashResult { @@ -269,12 +346,12 @@ class HashResult { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(assetId, other.assetId) && _deepEquals(error, other.error) && _deepEquals(hash, other.hash); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class CloudIdResult { @@ -308,12 +385,14 @@ class CloudIdResult { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(assetId, other.assetId) && + _deepEquals(error, other.error) && + _deepEquals(cloudId, other.cloudId); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class _PigeonCodec extends StandardMessageCodec { @@ -350,7 +429,7 @@ class _PigeonCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 129: - final int? value = readValue(buffer) as int?; + final value = readValue(buffer) as int?; return value == null ? null : PlatformAssetPlaybackStyle.values[value]; case 130: return PlatformAsset.decode(readValue(buffer)!); @@ -382,323 +461,215 @@ class NativeSyncApi { final String pigeonVar_messageChannelSuffix; Future shouldFullSync() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as bool?)!; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as bool; } Future getMediaChanges() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as SyncDelta?)!; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as SyncDelta; } Future checkpointSync() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future clearSyncCheckpoint() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future> getAssetIdsForAlbum(String albumId) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([albumId]); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as List?)!.cast(); - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return (pigeonVar_replyValue! as List).cast(); } Future> getAlbums() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final 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(); - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return (pigeonVar_replyValue! as List).cast(); } Future getAssetsCountSince(String albumId, int timestamp) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([albumId, timestamp]); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as int?)!; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as int; } Future> getAssetsForAlbum(String albumId, {int? updatedTimeCond}) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([albumId, updatedTimeCond]); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as List?)!.cast(); - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return (pigeonVar_replyValue! as List).cast(); } Future> hashAssets(List assetIds, {bool allowNetworkAccess = false}) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([assetIds, allowNetworkAccess]); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } 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(); - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return (pigeonVar_replyValue! as List).cast(); } Future cancelHashing() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelHashing$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future>> getTrashedAssets() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final 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 Map?)!.cast>(); - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return (pigeonVar_replyValue! as Map).cast>(); } Future> getCloudIdForAssetIds(List assetIds) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([assetIds]); - 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(); - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return (pigeonVar_replyValue! as List).cast(); } } diff --git a/mobile/lib/platform/network_api.g.dart b/mobile/lib/platform/network_api.g.dart index 0ecbb430d3..7fab476694 100644 --- a/mobile/lib/platform/network_api.g.dart +++ b/mobile/lib/platform/network_api.g.dart @@ -1,34 +1,91 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), 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 +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'dart:typed_data' show Float64List, Int32List, Int64List; -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); +Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; } bool _deepEquals(Object? a, Object? b) { + if (identical(a, b)) { + return true; + } + if (a is double && b is double) { + if (a.isNaN && b.isNaN) { + return true; + } + return a == 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]), - ); + if (a.length != b.length) { + return false; + } + for (final MapEntry entryA in a.entries) { + bool found = false; + for (final MapEntry entryB in b.entries) { + if (_deepEquals(entryA.key, entryB.key)) { + if (_deepEquals(entryA.value, entryB.value)) { + found = true; + break; + } else { + return false; + } + } + } + if (!found) { + return false; + } + } + return true; } return a == b; } +int _deepHash(Object? value) { + if (value is List) { + return Object.hashAll(value.map(_deepHash)); + } + if (value is Map) { + int result = 0; + for (final MapEntry entry in value.entries) { + result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); + } + return result; + } + if (value is double && value.isNaN) { + // Normalize NaN to a consistent hash. + return 0x7FF8000000000000.hashCode; + } + if (value is double && value == 0.0) { + // Normalize -0.0 to 0.0 so they have the same hash code. + return 0.0.hashCode; + } + return value.hashCode; +} + class ClientCertData { ClientCertData({required this.data, required this.password}); @@ -58,12 +115,12 @@ class ClientCertData { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(data, other.data) && _deepEquals(password, other.password); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class ClientCertPrompt { @@ -104,12 +161,15 @@ class ClientCertPrompt { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(title, other.title) && + _deepEquals(message, other.message) && + _deepEquals(cancel, other.cancel) && + _deepEquals(confirm, other.confirm); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class _PigeonCodec extends StandardMessageCodec { @@ -157,150 +217,96 @@ class NetworkApi { final String pigeonVar_messageChannelSuffix; Future addCertificate(ClientCertData clientData) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([clientData]); - 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future selectCertificate(ClientCertPrompt promptText) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([promptText]); - 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future removeCertificate() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future hasCertificate() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.hasCertificate$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as bool?)!; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as bool; } Future getClientPointer() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.getClientPointer$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final 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 int?)!; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as int; } Future setRequestHeaders(Map headers, List serverUrls, String? token) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.setRequestHeaders$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([headers, serverUrls, token]); - 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } } diff --git a/mobile/lib/platform/remote_image_api.g.dart b/mobile/lib/platform/remote_image_api.g.dart index 474f033f1f..5239cb3e45 100644 --- a/mobile/lib/platform/remote_image_api.g.dart +++ b/mobile/lib/platform/remote_image_api.g.dart @@ -1,18 +1,29 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), 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 +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'dart:typed_data' show Float64List, Int32List, Int64List; -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); +Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; } class _PigeonCodec extends StandardMessageCodec { @@ -50,76 +61,54 @@ class RemoteImageApi { final String pigeonVar_messageChannelSuffix; Future?> requestImage(String url, {required int requestId, required bool preferEncoded}) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([url, requestId, preferEncoded]); - 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 (pigeonVar_replyList[0] as Map?)?.cast(); - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + return (pigeonVar_replyValue as Map?)?.cast(); } Future cancelRequest(int requestId) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.RemoteImageApi.cancelRequest$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([requestId]); - 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; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future clearCache() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.RemoteImageApi.clearCache$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final 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 int?)!; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as int; } } diff --git a/mobile/lib/presentation/pages/drift_activities.page.dart b/mobile/lib/presentation/pages/drift_activities.page.dart index fa5737443f..b998e10dc2 100644 --- a/mobile/lib/presentation/pages/drift_activities.page.dart +++ b/mobile/lib/presentation/pages/drift_activities.page.dart @@ -5,11 +5,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/activities/comment_bubble.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/drift_activity_text_field.dart'; import 'package:immich_mobile/providers/activity.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; +import 'package:immich_mobile/widgets/activities/comment_bubble.dart'; @RoutePage() class DriftActivitiesPage extends HookConsumerWidget { @@ -21,8 +21,8 @@ class DriftActivitiesPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final activityNotifier = ref.read(albumActivityProvider(album.id, assetId).notifier); - final activities = ref.watch(albumActivityProvider(album.id, assetId)); + final activityNotifier = ref.read(albumActivityProvider((album.id, assetId)).notifier); + final activities = ref.watch(albumActivityProvider((album.id, assetId))); final listViewScrollController = useScrollController(); void scrollToBottom() { diff --git a/mobile/lib/presentation/pages/drift_album_options.page.dart b/mobile/lib/presentation/pages/drift_album_options.page.dart index 061edbaf26..1a516426b5 100644 --- a/mobile/lib/presentation/pages/drift_album_options.page.dart +++ b/mobile/lib/presentation/pages/drift_album_options.page.dart @@ -34,7 +34,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { final isOwner = album.ownerId == userId; void showErrorMessage() { - context.pop(); + ContextHelper(context).pop(); ImmichToast.show( context: context, msg: "shared_album_section_people_action_error".t(context: context), @@ -60,7 +60,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { showErrorMessage(); } - context.pop(); + ContextHelper(context).pop(); } Future addUsers() async { diff --git a/mobile/lib/presentation/pages/drift_map.page.dart b/mobile/lib/presentation/pages/drift_map.page.dart index 96384c97e5..97062b88ab 100644 --- a/mobile/lib/presentation/pages/drift_map.page.dart +++ b/mobile/lib/presentation/pages/drift_map.page.dart @@ -33,7 +33,7 @@ class DriftMapPage extends StatelessWidget { top: 70, child: IconButton.filled( color: Colors.white, - onPressed: () => context.pop(), + onPressed: () => ContextHelper(context).pop(), icon: const Icon(Icons.arrow_back_ios_new_rounded), style: IconButton.styleFrom( padding: const EdgeInsets.all(8), diff --git a/mobile/lib/presentation/pages/drift_memory.page.dart b/mobile/lib/presentation/pages/drift_memory.page.dart index 3f8879c91d..846f062501 100644 --- a/mobile/lib/presentation/pages/drift_memory.page.dart +++ b/mobile/lib/presentation/pages/drift_memory.page.dart @@ -207,6 +207,11 @@ class DriftMemoryPage extends HookConsumerWidget { WidgetsBinding.instance.addPostFrameCallback((_) { DriftMemoryPage.setMemory(ref, memories[pageNumber]); }); + + // Update currentAsset to the first asset of the new memory + if (memories[pageNumber].assets.isNotEmpty) { + currentAsset.value = memories[pageNumber].assets.first; + } } currentAssetPage.value = 0; diff --git a/mobile/lib/presentation/pages/drift_people_collection.page.dart b/mobile/lib/presentation/pages/drift_people_collection.page.dart index f73dac3af2..32bbd7e60b 100644 --- a/mobile/lib/presentation/pages/drift_people_collection.page.dart +++ b/mobile/lib/presentation/pages/drift_people_collection.page.dart @@ -79,6 +79,7 @@ class _DriftPeopleCollectionPageState extends ConsumerState { return PersonOptionSheet( onEditName: () async { await handleEditName(context); - context.pop(); + ContextHelper(context).pop(); }, onEditBirthday: () async { await handleEditBirthday(context); - context.pop(); + ContextHelper(context).pop(); }, birthdayExists: _person.birthDate != null, ); diff --git a/mobile/lib/presentation/pages/edit/drift_edit.page.dart b/mobile/lib/presentation/pages/edit/drift_edit.page.dart new file mode 100644 index 0000000000..8f7d874983 --- /dev/null +++ b/mobile/lib/presentation/pages/edit/drift_edit.page.dart @@ -0,0 +1,399 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:auto_route/auto_route.dart'; +import 'package:crop_image/crop_image.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/aspect_ratios.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart'; +import 'package:immich_mobile/providers/theme.provider.dart'; +import 'package:immich_mobile/theme/theme_data.dart'; +import 'package:immich_mobile/utils/editor.utils.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:openapi/api.dart' show RotateParameters, MirrorParameters, MirrorAxis; + +@RoutePage() +class DriftEditImagePage extends ConsumerStatefulWidget { + final Image image; + final Future Function(List edits) applyEdits; + + const DriftEditImagePage({super.key, required this.image, required this.applyEdits}); + + @override + ConsumerState createState() => _DriftEditImagePageState(); +} + +class _DriftEditImagePageState extends ConsumerState with TickerProviderStateMixin { + Future _saveEditedImage() async { + ref.read(editorStateProvider.notifier).setIsEditing(true); + + final editorState = ref.read(editorStateProvider); + final cropParameters = convertRectToCropParameters( + editorState.crop, + editorState.originalWidth, + editorState.originalHeight, + ); + final edits = []; + + if (cropParameters.width != editorState.originalWidth || cropParameters.height != editorState.originalHeight) { + edits.add(CropEdit(cropParameters)); + } + + if (editorState.flipHorizontal) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal))); + } + + if (editorState.flipVertical) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical))); + } + + final normalizedRotation = (editorState.rotationAngle % 360 + 360) % 360; + if (normalizedRotation != 0) { + edits.add(RotateEdit(RotateParameters(angle: normalizedRotation))); + } + + try { + await widget.applyEdits(edits); + ImmichToast.show(context: context, msg: 'success'.tr(), toastType: ToastType.success); + Navigator.of(context).pop(); + } catch (e) { + ImmichToast.show(context: context, msg: 'error_title'.tr(), toastType: ToastType.error); + } finally { + ref.read(editorStateProvider.notifier).setIsEditing(false); + } + } + + Future _showDiscardChangesDialog() { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('editor_discard_edits_title'.tr()), + content: Text('editor_discard_edits_prompt'.tr()), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + style: ButtonStyle( + foregroundColor: WidgetStateProperty.all(context.themeData.colorScheme.onSurfaceVariant), + ), + child: Text('cancel'.tr()), + ), + TextButton(onPressed: () => Navigator.of(context).pop(true), child: Text('confirm'.tr())), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final hasUnsavedEdits = ref.watch(editorStateProvider.select((state) => state.hasUnsavedEdits)); + + return PopScope( + canPop: !hasUnsavedEdits, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + final shouldDiscard = await _showDiscardChangesDialog() ?? false; + if (shouldDiscard && mounted) { + Navigator.of(context).pop(); + } + }, + child: Theme( + data: getThemeData(colorScheme: ref.watch(immichThemeProvider).dark, locale: context.locale), + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.black, + title: Text("edit".tr()), + leading: ImmichCloseButton(onPressed: () => Navigator.of(context).maybePop()), + actions: [_SaveEditsButton(onSave: _saveEditedImage)], + ), + backgroundColor: Colors.black, + body: SafeArea( + bottom: false, + child: Column( + children: [ + Expanded(child: _EditorPreview(image: widget.image)), + AnimatedSize( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + alignment: Alignment.bottomCenter, + clipBehavior: Clip.none, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: ref.watch(immichThemeProvider).dark.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + _TransformControls(), + Padding( + padding: EdgeInsets.only(bottom: 36, left: 24, right: 24), + child: Row(children: [Spacer(), _ResetEditsButton()]), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _AspectRatioButton extends StatelessWidget { + final AspectRatioPreset ratio; + final bool isSelected; + final VoidCallback onPressed; + + const _AspectRatioButton({required this.ratio, required this.isSelected, required this.onPressed}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.max, + children: [ + IconButton( + iconSize: 36, + icon: Transform.rotate( + angle: ratio.iconRotated ? pi / 2 : 0, + child: Icon(ratio.icon, color: isSelected ? context.primaryColor : context.themeData.iconTheme.color), + ), + onPressed: onPressed, + ), + Text(ratio.label, style: context.textTheme.displayMedium), + ], + ); + } +} + +class _AspectRatioSelector extends ConsumerWidget { + const _AspectRatioSelector(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final editorState = ref.watch(editorStateProvider); + final editorNotifier = ref.read(editorStateProvider.notifier); + + // the whole crop view is rotated, so we need to swap the aspect ratio when the rotation is 90 or 270 degrees + double? selectedAspectRatio = editorState.aspectRatio; + if (editorState.rotationAngle % 180 != 0 && selectedAspectRatio != null) { + selectedAspectRatio = 1 / selectedAspectRatio; + } + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: AspectRatioPreset.values.map((entry) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: _AspectRatioButton( + ratio: entry, + isSelected: selectedAspectRatio == entry.ratio, + onPressed: () => editorNotifier.setAspectRatio(entry.ratio), + ), + ); + }).toList(), + ), + ); + } +} + +class _TransformControls extends ConsumerWidget { + const _TransformControls(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final editorNotifier = ref.read(editorStateProvider.notifier); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 20, right: 20, top: 20, bottom: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ImmichIconButton( + icon: Icons.rotate_left, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.rotateCCW, + ), + const SizedBox(width: 8), + ImmichIconButton( + icon: Icons.rotate_right, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.rotateCW, + ), + ], + ), + Row( + children: [ + ImmichIconButton( + icon: Icons.flip, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.flipHorizontally, + ), + const SizedBox(width: 8), + Transform.rotate( + angle: pi / 2, + child: ImmichIconButton( + icon: Icons.flip, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.flipVertically, + ), + ), + ], + ), + ], + ), + ), + const _AspectRatioSelector(), + const SizedBox(height: 32), + ], + ); + } +} + +class _SaveEditsButton extends ConsumerWidget { + final VoidCallback onSave; + + const _SaveEditsButton({required this.onSave}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isApplyingEdits = ref.watch(editorStateProvider.select((state) => state.isApplyingEdits)); + final hasUnsavedEdits = ref.watch(editorStateProvider.select((state) => state.hasUnsavedEdits)); + + return isApplyingEdits + ? const Padding( + padding: EdgeInsets.all(8.0), + child: SizedBox(width: 28, height: 28, child: CircularProgressIndicator(strokeWidth: 2.5)), + ) + : ImmichIconButton( + icon: Icons.done_rounded, + color: ImmichColor.primary, + variant: ImmichVariant.ghost, + disabled: !hasUnsavedEdits, + onPressed: onSave, + ); + } +} + +class _ResetEditsButton extends ConsumerWidget { + const _ResetEditsButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final editorState = ref.watch(editorStateProvider); + final editorNotifier = ref.read(editorStateProvider.notifier); + + return ImmichTextButton( + labelText: 'reset'.tr(), + onPressed: editorNotifier.resetEdits, + variant: ImmichVariant.ghost, + expanded: false, + disabled: !editorState.hasEdits || editorState.isApplyingEdits, + ); + } +} + +class _EditorPreview extends ConsumerStatefulWidget { + final Image image; + + const _EditorPreview({required this.image}); + + @override + ConsumerState<_EditorPreview> createState() => _EditorPreviewState(); +} + +class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProviderStateMixin { + late final CropController cropController; + + @override + void initState() { + super.initState(); + + cropController = CropController(); + cropController.crop = ref.read(editorStateProvider.select((state) => state.crop)); + cropController.addListener(onCrop); + } + + void onCrop() { + if (!mounted || cropController.crop == ref.read(editorStateProvider).crop) { + return; + } + + ref.read(editorStateProvider.notifier).setCrop(cropController.crop); + } + + @override + void dispose() { + cropController.removeListener(onCrop); + cropController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final editorState = ref.watch(editorStateProvider); + final editorNotifier = ref.read(editorStateProvider.notifier); + + ref.listen(editorStateProvider, (_, current) { + cropController.aspectRatio = current.aspectRatio; + + if (cropController.crop != current.crop) { + cropController.crop = current.crop; + } + }); + + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + // Calculate the bounding box size needed for the rotated container + final baseWidth = constraints.maxWidth * 0.9; + final baseHeight = constraints.maxHeight * 0.95; + + return Center( + child: AnimatedRotation( + turns: editorState.rotationAngle / 360, + duration: editorState.animationDuration, + curve: Curves.easeInOut, + onEnd: editorNotifier.normalizeRotation, + child: Transform( + alignment: Alignment.center, + transform: Matrix4.identity() + ..scaleByDouble( + editorState.flipHorizontal ? -1.0 : 1.0, + editorState.flipVertical ? -1.0 : 1.0, + 1.0, + 1.0, + ), + child: Container( + padding: const EdgeInsets.all(10), + width: (editorState.rotationAngle % 180 == 0) ? baseWidth : baseHeight, + height: (editorState.rotationAngle % 180 == 0) ? baseHeight : baseWidth, + child: CropImage(controller: cropController, image: widget.image, gridColor: Colors.white), + ), + ), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/presentation/pages/edit/editor.provider.dart b/mobile/lib/presentation/pages/edit/editor.provider.dart new file mode 100644 index 0000000000..21b5268912 --- /dev/null +++ b/mobile/lib/presentation/pages/edit/editor.provider.dart @@ -0,0 +1,210 @@ +import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/utils/editor.utils.dart'; + +final editorStateProvider = NotifierProvider(EditorProvider.new); + +class EditorProvider extends Notifier { + @override + EditorState build() { + return const EditorState(); + } + + void clear() { + state = const EditorState(); + } + + void init(List edits, ExifInfo exifInfo) { + clear(); + + final existingCrop = edits.whereType().firstOrNull; + + final originalWidth = exifInfo.isFlipped ? exifInfo.height : exifInfo.width; + final originalHeight = exifInfo.isFlipped ? exifInfo.width : exifInfo.height; + + Rect crop = existingCrop != null && originalWidth != null && originalHeight != null + ? convertCropParametersToRect(existingCrop.parameters, originalWidth, originalHeight) + : const Rect.fromLTRB(0, 0, 1, 1); + + final transform = normalizeTransformEdits(edits); + + state = state.copyWith( + originalWidth: originalWidth, + originalHeight: originalHeight, + crop: crop, + flipHorizontal: transform.mirrorHorizontal, + flipVertical: transform.mirrorVertical, + ); + + _animateRotation(transform.rotation.toInt(), duration: Duration.zero); + } + + void _animateRotation(int angle, {Duration duration = const Duration(milliseconds: 300)}) { + state = state.copyWith(rotationAngle: angle, animationDuration: duration); + } + + void normalizeRotation() { + final normalizedAngle = ((state.rotationAngle % 360) + 360) % 360; + if (normalizedAngle != state.rotationAngle) { + state = state.copyWith(rotationAngle: normalizedAngle, animationDuration: Duration.zero); + } + } + + void setIsEditing(bool isApplyingEdits) { + state = state.copyWith(isApplyingEdits: isApplyingEdits); + } + + void setCrop(Rect crop) { + state = state.copyWith(crop: crop, hasUnsavedEdits: true); + } + + void setAspectRatio(double? aspectRatio) { + if (aspectRatio != null && state.rotationAngle % 180 != 0) { + // When rotated 90 or 270 degrees, swap width and height for aspect ratio calculations + aspectRatio = 1 / aspectRatio; + } + + state = state.copyWith(aspectRatio: aspectRatio); + } + + void resetEdits() { + _animateRotation(0); + + state = state.copyWith( + flipHorizontal: false, + flipVertical: false, + crop: const Rect.fromLTRB(0, 0, 1, 1), + aspectRatio: null, + hasUnsavedEdits: true, + ); + } + + void rotateCCW() { + _animateRotation(state.rotationAngle - 90); + state = state.copyWith(hasUnsavedEdits: true); + } + + void rotateCW() { + _animateRotation(state.rotationAngle + 90); + state = state.copyWith(hasUnsavedEdits: true); + } + + void flipHorizontally() { + if (state.rotationAngle % 180 != 0) { + // When rotated 90 or 270 degrees, flipping horizontally is equivalent to flipping vertically + state = state.copyWith(flipVertical: !state.flipVertical, hasUnsavedEdits: true); + } else { + state = state.copyWith(flipHorizontal: !state.flipHorizontal, hasUnsavedEdits: true); + } + } + + void flipVertically() { + if (state.rotationAngle % 180 != 0) { + // When rotated 90 or 270 degrees, flipping vertically is equivalent to flipping horizontally + state = state.copyWith(flipHorizontal: !state.flipHorizontal, hasUnsavedEdits: true); + } else { + state = state.copyWith(flipVertical: !state.flipVertical, hasUnsavedEdits: true); + } + } +} + +class EditorState { + final bool isApplyingEdits; + + final int rotationAngle; + final bool flipHorizontal; + final bool flipVertical; + final Rect crop; + final double? aspectRatio; + + final int originalWidth; + final int originalHeight; + + final Duration animationDuration; + + final bool hasUnsavedEdits; + + const EditorState({ + bool? isApplyingEdits, + int? rotationAngle, + bool? flipHorizontal, + bool? flipVertical, + Rect? crop, + this.aspectRatio, + int? originalWidth, + int? originalHeight, + Duration? animationDuration, + bool? hasUnsavedEdits, + }) : isApplyingEdits = isApplyingEdits ?? false, + rotationAngle = rotationAngle ?? 0, + flipHorizontal = flipHorizontal ?? false, + flipVertical = flipVertical ?? false, + animationDuration = animationDuration ?? Duration.zero, + originalWidth = originalWidth ?? 0, + originalHeight = originalHeight ?? 0, + crop = crop ?? const Rect.fromLTRB(0, 0, 1, 1), + hasUnsavedEdits = hasUnsavedEdits ?? false; + + EditorState copyWith({ + bool? isApplyingEdits, + int? rotationAngle, + bool? flipHorizontal, + bool? flipVertical, + double? aspectRatio = double.infinity, + int? originalWidth, + int? originalHeight, + Duration? animationDuration, + Rect? crop, + bool? hasUnsavedEdits, + }) { + return EditorState( + isApplyingEdits: isApplyingEdits ?? this.isApplyingEdits, + rotationAngle: rotationAngle ?? this.rotationAngle, + flipHorizontal: flipHorizontal ?? this.flipHorizontal, + flipVertical: flipVertical ?? this.flipVertical, + aspectRatio: aspectRatio == double.infinity ? this.aspectRatio : aspectRatio, + animationDuration: animationDuration ?? this.animationDuration, + originalWidth: originalWidth ?? this.originalWidth, + originalHeight: originalHeight ?? this.originalHeight, + crop: crop ?? this.crop, + hasUnsavedEdits: hasUnsavedEdits ?? this.hasUnsavedEdits, + ); + } + + bool get hasEdits { + return rotationAngle != 0 || flipHorizontal || flipVertical || crop != const Rect.fromLTRB(0, 0, 1, 1); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is EditorState && + other.isApplyingEdits == isApplyingEdits && + other.rotationAngle == rotationAngle && + other.flipHorizontal == flipHorizontal && + other.flipVertical == flipVertical && + other.crop == crop && + other.aspectRatio == aspectRatio && + other.originalWidth == originalWidth && + other.originalHeight == originalHeight && + other.animationDuration == animationDuration && + other.hasUnsavedEdits == hasUnsavedEdits; + } + + @override + int get hashCode { + return isApplyingEdits.hashCode ^ + rotationAngle.hashCode ^ + flipHorizontal.hashCode ^ + flipVertical.hashCode ^ + crop.hashCode ^ + aspectRatio.hashCode ^ + originalWidth.hashCode ^ + originalHeight.hashCode ^ + animationDuration.hashCode ^ + hasUnsavedEdits.hashCode; + } +} diff --git a/mobile/lib/presentation/pages/editing/drift_crop.page.dart b/mobile/lib/presentation/pages/editing/drift_crop.page.dart deleted file mode 100644 index a213e4c640..0000000000 --- a/mobile/lib/presentation/pages/editing/drift_crop.page.dart +++ /dev/null @@ -1,179 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:crop_image/crop_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart'; -import 'package:immich_ui/immich_ui.dart'; - -/// A widget for cropping an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to crop an image and then navigate to the [EditImagePage] with the -/// cropped image. - -@RoutePage() -class DriftCropImagePage extends HookWidget { - final Image image; - final BaseAsset asset; - const DriftCropImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final cropController = useCropController(); - final aspectRatio = useState(null); - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("crop".tr()), - leading: const ImmichCloseButton(), - actions: [ - ImmichIconButton( - icon: Icons.done_rounded, - color: ImmichColor.primary, - variant: ImmichVariant.ghost, - onPressed: () async { - final croppedImage = await cropController.croppedImage(); - unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: SafeArea( - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - Container( - padding: const EdgeInsets.only(top: 20), - width: constraints.maxWidth * 0.9, - height: constraints.maxHeight * 0.6, - child: CropImage(controller: cropController, image: image, gridColor: Colors.white), - ), - Expanded( - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ImmichIconButton( - icon: Icons.rotate_left, - variant: ImmichVariant.ghost, - color: ImmichColor.secondary, - onPressed: () => cropController.rotateLeft(), - ), - ImmichIconButton( - icon: Icons.rotate_right, - variant: ImmichVariant.ghost, - color: ImmichColor.secondary, - onPressed: () => cropController.rotateRight(), - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: null, - label: 'Free', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 1.0, - label: '1:1', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 16.0 / 9.0, - label: '16:9', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 3.0 / 2.0, - label: '3:2', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 7.0 / 5.0, - label: '7:5', - ), - ], - ), - ], - ), - ), - ), - ), - ], - ); - }, - ), - ), - ); - } -} - -class _AspectRatioButton extends StatelessWidget { - final CropController cropController; - final ValueNotifier aspectRatio; - final double? ratio; - final String label; - - const _AspectRatioButton({ - required this.cropController, - required this.aspectRatio, - required this.ratio, - required this.label, - }); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(switch (label) { - 'Free' => Icons.crop_free_rounded, - '1:1' => Icons.crop_square_rounded, - '16:9' => Icons.crop_16_9_rounded, - '3:2' => Icons.crop_3_2_rounded, - '7:5' => Icons.crop_7_5_rounded, - _ => Icons.crop_free_rounded, - }, color: aspectRatio.value == ratio ? context.primaryColor : context.themeData.iconTheme.color), - onPressed: () { - cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9); - aspectRatio.value = ratio; - cropController.aspectRatio = ratio; - }, - ), - Text(label, style: context.textTheme.displayMedium), - ], - ); - } -} diff --git a/mobile/lib/presentation/pages/editing/drift_edit.page.dart b/mobile/lib/presentation/pages/editing/drift_edit.page.dart deleted file mode 100644 index 6d4ea4d3a6..0000000000 --- a/mobile/lib/presentation/pages/editing/drift_edit.page.dart +++ /dev/null @@ -1,153 +0,0 @@ -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/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/foreground_upload.service.dart'; -import 'package:immich_mobile/utils/image_converter.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as p; - -/// A stateless widget that provides functionality for editing an image. -/// -/// This widget allows users to edit an image provided either as an [Asset] or -/// directly as an [Image]. It ensures that exactly one of these is provided. -/// -/// It also includes a conversion method to convert an [Image] to a [Uint8List] to save the image on the user's phone -/// They automatically navigate to the [HomePage] with the edited image saved and they eventually get backed up to the server. -@immutable -@RoutePage() -class DriftEditImagePage extends ConsumerWidget { - final BaseAsset asset; - final Image image; - final bool isEdited; - - const DriftEditImagePage({super.key, required this.asset, required this.image, required this.isEdited}); - - void _exitEditing(BuildContext context) { - // this assumes that the only way to get to this page is from the AssetViewerRoute - context.navigator.popUntil((route) => route.data?.name == AssetViewerRoute.name); - } - - Future _saveEditedImage(BuildContext context, BaseAsset asset, Image image, WidgetRef ref) async { - try { - final Uint8List imageData = await imageToUint8List(image); - LocalAsset? localAsset; - - try { - localAsset = await ref - .read(fileMediaRepositoryProvider) - .saveLocalAsset(imageData, title: "${p.withoutExtension(asset.name)}_edited.jpg"); - } on PlatformException catch (e) { - // OS might not return the saved image back, so we handle that gracefully - // This can happen if app does not have full library access - Logger("SaveEditedImage").warning("Failed to retrieve the saved image back from OS", e); - } - - unawaited(ref.read(backgroundSyncProvider).syncLocal(full: true)); - _exitEditing(context); - ImmichToast.show(durationInSecond: 3, context: context, msg: 'Image Saved!'); - - if (localAsset == null) { - return; - } - - await ref.read(foregroundUploadServiceProvider).uploadManual([localAsset]); - } catch (e) { - ImmichToast.show( - durationInSecond: 6, - context: context, - msg: "error_saving_image".tr(namedArgs: {'error': e.toString()}), - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: Text("edit".tr()), - backgroundColor: context.scaffoldBackgroundColor, - leading: IconButton( - icon: Icon(Icons.close_rounded, color: context.primaryColor, size: 24), - onPressed: () => _exitEditing(context), - ), - actions: [ - TextButton( - onPressed: isEdited ? () => _saveEditedImage(context, asset, image, ref) : null, - child: Text("save_to_gallery".tr(), style: TextStyle(color: isEdited ? context.primaryColor : Colors.grey)), - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9), - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(7)), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.2), - spreadRadius: 2, - blurRadius: 10, - offset: const Offset(0, 3), - ), - ], - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(7)), - child: Image(image: image.image, fit: BoxFit.contain), - ), - ), - ), - ), - bottomNavigationBar: Container( - height: 70, - margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10), - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(30)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.crop_rotate_rounded, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(DriftCropImageRoute(asset: asset, image: image)); - }, - ), - Text("crop".tr(), style: context.textTheme.displayMedium), - ], - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.filter, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(DriftFilterImageRoute(asset: asset, image: image)); - }, - ), - Text("filter".tr(), style: context.textTheme.displayMedium), - ], - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/presentation/pages/editing/drift_filter.page.dart b/mobile/lib/presentation/pages/editing/drift_filter.page.dart deleted file mode 100644 index 8198a41bbe..0000000000 --- a/mobile/lib/presentation/pages/editing/drift_filter.page.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui; - -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:immich_mobile/constants/filters.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/routing/router.dart'; - -/// A widget for filtering an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to add filters to an image and then navigate to the [EditImagePage] with the -/// final composition.' -@RoutePage() -class DriftFilterImagePage extends HookWidget { - final Image image; - final BaseAsset asset; - - const DriftFilterImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final colorFilter = useState(filters[0]); - final selectedFilterIndex = useState(0); - - Future createFilteredImage(ui.Image inputImage, ColorFilter filter) { - final completer = Completer(); - final size = Size(inputImage.width.toDouble(), inputImage.height.toDouble()); - final recorder = ui.PictureRecorder(); - final canvas = Canvas(recorder); - - final paint = Paint()..colorFilter = filter; - canvas.drawImage(inputImage, Offset.zero, paint); - - recorder.endRecording().toImage(size.width.round(), size.height.round()).then((image) { - completer.complete(image); - }); - - return completer.future; - } - - void applyFilter(ColorFilter filter, int index) { - colorFilter.value = filter; - selectedFilterIndex.value = index; - } - - Future applyFilterAndConvert(ColorFilter filter) async { - final completer = Completer(); - image.image - .resolve(ImageConfiguration.empty) - .addListener( - ImageStreamListener((ImageInfo info, bool _) { - completer.complete(info.image); - }), - ); - final uiImage = await completer.future; - - final filteredUiImage = await createFilteredImage(uiImage, filter); - final byteData = await filteredUiImage.toByteData(format: ui.ImageByteFormat.png); - final pngBytes = byteData!.buffer.asUint8List(); - - return Image.memory(pngBytes, fit: BoxFit.contain); - } - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("filter".tr()), - leading: CloseButton(color: context.primaryColor), - actions: [ - IconButton( - icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), - onPressed: () async { - final filteredImage = await applyFilterAndConvert(colorFilter.value); - unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: filteredImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Column( - children: [ - SizedBox( - height: context.height * 0.7, - child: Center( - child: ColorFiltered(colorFilter: colorFilter.value, child: image), - ), - ), - SizedBox( - height: 120, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: filters.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: _FilterButton( - image: image, - label: filterNames[index], - filter: filters[index], - isSelected: selectedFilterIndex.value == index, - onTap: () => applyFilter(filters[index], index), - ), - ); - }, - ), - ), - ], - ), - ); - } -} - -class _FilterButton extends StatelessWidget { - final Image image; - final String label; - final ColorFilter filter; - final bool isSelected; - final VoidCallback onTap; - - const _FilterButton({ - required this.image, - required this.label, - required this.filter, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - GestureDetector( - onTap: onTap, - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10)), - border: isSelected ? Border.all(color: context.primaryColor, width: 3) : null, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(10)), - child: ColorFiltered( - colorFilter: filter, - child: FittedBox(fit: BoxFit.cover, child: image), - ), - ), - ), - ), - const SizedBox(height: 10), - Text(label, style: context.themeData.textTheme.bodyMedium), - ], - ); - } -} diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 701a6ff74a..881daf9d38 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -6,11 +6,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/domain/models/tag.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; @@ -52,23 +52,20 @@ class DriftSearchPage extends HookConsumerWidget { : 'file_name_or_extension'.t(context: context), ); final textSearchController = useTextEditingController(); - final preFilter = ref.watch(searchPreFilterProvider); final filter = useState( SearchFilter( - people: preFilter?.people ?? {}, - location: preFilter?.location ?? SearchLocationFilter(), - camera: preFilter?.camera ?? SearchCameraFilter(), - date: preFilter?.date ?? SearchDateFilter(), - display: preFilter?.display ?? SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - rating: preFilter?.rating ?? SearchRatingFilter(), - mediaType: preFilter?.mediaType ?? AssetType.other, + people: {}, + location: SearchLocationFilter(), + camera: SearchCameraFilter(), + date: SearchDateFilter(), + display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), + rating: SearchRatingFilter(), + mediaType: AssetType.other, language: "${context.locale.languageCode}-${context.locale.countryCode}", - assetId: preFilter?.assetId, - tagIds: preFilter?.tagIds ?? [], + tagIds: [], ), ); - final previousFilter = useState(null); final dateInputFilter = useState(null); final peopleCurrentFilterWidget = useState(null); @@ -82,64 +79,58 @@ class DriftSearchPage extends HookConsumerWidget { final userPreferences = ref.watch(userMetadataPreferencesProvider); - searchFilter(SearchFilter filter) { - if (preFilter == null && filter == previousFilter.value) { + search(SearchFilter f) { + if (f == filter.value) { return; } + filter.value = f; + ref.read(paginatedSearchProvider.notifier).clear(); - if (filter.isEmpty) { - previousFilter.value = null; - return; + if (!f.isEmpty) { + unawaited(ref.read(paginatedSearchProvider.notifier).search(f)); } - - unawaited(ref.read(paginatedSearchProvider.notifier).search(filter)); - previousFilter.value = filter; } - search() => searchFilter(filter.value); - loadMoreSearchResults() { unawaited(ref.read(paginatedSearchProvider.notifier).search(filter.value)); } - searchPreFilter() { - if (preFilter != null) { - Future.delayed(Duration.zero, () { - searchFilter(preFilter); - - if (preFilter.location.city != null) { - locationCurrentFilterWidget.value = Text(preFilter.location.city!, style: context.textTheme.labelLarge); - } - }); - } - } - + // TODO: Use ref.listen with `fireImmediately` in the new riverpod version. + final preFilter = ref.watch(searchPreFilterProvider); useEffect(() { - Future.microtask(() => ref.invalidate(paginatedSearchProvider)); - searchPreFilter(); + if (preFilter == null) { + return null; + } + + Future.microtask(() { + textSearchController.clear(); + search(preFilter); + if (preFilter.location.city != null) { + locationCurrentFilterWidget.value = Text(preFilter.location.city!, style: context.textTheme.labelLarge); + } + }); return null; }, [preFilter]); showPeoplePicker() { - handleOnSelect(Set value) { - filter.value = filter.value.copyWith(people: value); + var people = filter.value.people; - final label = value.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context)).join(', '); - if (label.isNotEmpty) { - peopleCurrentFilterWidget.value = Text(label, style: context.textTheme.labelLarge); - } else { - peopleCurrentFilterWidget.value = null; - } + handleOnSelect(Set value) { + people = value; } handleClear() { - filter.value = filter.value.copyWith(people: {}); - peopleCurrentFilterWidget.value = null; - search(); + search(filter.value.copyWith(people: {})); + } + + handleApply() { + final label = people.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context)).join(', '); + peopleCurrentFilterWidget.value = label.isNotEmpty ? Text(label, style: context.textTheme.labelLarge) : null; + search(filter.value.copyWith(people: people)); } showFilterBottomSheet( @@ -150,7 +141,7 @@ class DriftSearchPage extends HookConsumerWidget { child: FilterBottomSheetScaffold( title: 'search_filter_people_title'.t(context: context), expanded: true, - onSearch: search, + onSearch: handleApply, onClear: handleClear, child: PeoplePicker(onSelect: handleOnSelect, filter: filter.value.people), ), @@ -159,23 +150,22 @@ class DriftSearchPage extends HookConsumerWidget { } showTagPicker() { + var tagIds = filter.value.tagIds ?? []; + String tagLabel = ''; + handleOnSelect(Iterable tags) { - filter.value = filter.value.copyWith(tagIds: tags.map((t) => t.id).toList()); - final label = tags.map((t) => t.value).join(', '); - if (label.isEmpty) { - tagCurrentFilterWidget.value = null; - } else { - tagCurrentFilterWidget.value = Text( - label.isEmpty ? 'tags'.t(context: context) : label, - style: context.textTheme.labelLarge, - ); - } + tagIds = tags.map((t) => t.id).toList(); + tagLabel = tags.map((t) => t.value).join(', '); } handleClear() { - filter.value = filter.value.copyWith(tagIds: []); tagCurrentFilterWidget.value = null; - search(); + search(filter.value.copyWith(tagIds: [])); + } + + handleApply() { + tagCurrentFilterWidget.value = tagLabel.isNotEmpty ? Text(tagLabel, style: context.textTheme.labelLarge) : null; + search(filter.value.copyWith(tagIds: tagIds)); } showFilterBottomSheet( @@ -186,7 +176,7 @@ class DriftSearchPage extends HookConsumerWidget { child: FilterBottomSheetScaffold( title: 'search_filter_tags_title'.t(context: context), expanded: true, - onSearch: search, + onSearch: handleApply, onClear: handleClear, child: TagPicker(onSelect: handleOnSelect, filter: (filter.value.tagIds ?? []).toSet()), ), @@ -195,32 +185,27 @@ class DriftSearchPage extends HookConsumerWidget { } showLocationPicker() { + var location = filter.value.location; + handleOnSelect(Map value) { - filter.value = filter.value.copyWith( - location: SearchLocationFilter(country: value['country'], city: value['city'], state: value['state']), - ); - - final locationText = []; - if (value['country'] != null) { - locationText.add(value['country']!); - } - - if (value['state'] != null) { - locationText.add(value['state']!); - } - - if (value['city'] != null) { - locationText.add(value['city']!); - } - - locationCurrentFilterWidget.value = Text(locationText.join(', '), style: context.textTheme.labelLarge); + location = SearchLocationFilter(country: value['country'], city: value['city'], state: value['state']); } handleClear() { - filter.value = filter.value.copyWith(location: SearchLocationFilter()); - locationCurrentFilterWidget.value = null; - search(); + search(filter.value.copyWith(location: SearchLocationFilter())); + } + + handleApply() { + final locationText = [ + if (location.country != null) location.country!, + if (location.state != null) location.state!, + if (location.city != null) location.city!, + ]; + locationCurrentFilterWidget.value = locationText.isNotEmpty + ? Text(locationText.join(', '), style: context.textTheme.labelLarge) + : null; + search(filter.value.copyWith(location: location)); } showFilterBottomSheet( @@ -229,7 +214,7 @@ class DriftSearchPage extends HookConsumerWidget { isDismissible: true, child: FilterBottomSheetScaffold( title: 'search_filter_location_title'.t(context: context), - onSearch: search, + onSearch: handleApply, onClear: handleClear, child: Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), @@ -246,22 +231,24 @@ class DriftSearchPage extends HookConsumerWidget { } showCameraPicker() { - handleOnSelect(Map value) { - filter.value = filter.value.copyWith( - camera: SearchCameraFilter(make: value['make'], model: value['model']), - ); + var camera = filter.value.camera; - cameraCurrentFilterWidget.value = Text( - '${value['make'] ?? ''} ${value['model'] ?? ''}', - style: context.textTheme.labelLarge, - ); + handleOnSelect(Map value) { + camera = SearchCameraFilter(make: value['make'], model: value['model']); } handleClear() { - filter.value = filter.value.copyWith(camera: SearchCameraFilter()); - cameraCurrentFilterWidget.value = null; - search(); + search(filter.value.copyWith(camera: SearchCameraFilter())); + } + + handleApply() { + final make = camera.make ?? ''; + final model = camera.model ?? ''; + cameraCurrentFilterWidget.value = (make.isNotEmpty || model.isNotEmpty) + ? Text('$make $model', style: context.textTheme.labelLarge) + : null; + search(filter.value.copyWith(camera: camera)); } showFilterBottomSheet( @@ -270,7 +257,7 @@ class DriftSearchPage extends HookConsumerWidget { isDismissible: true, child: FilterBottomSheetScaffold( title: 'search_filter_camera_title'.t(context: context), - onSearch: search, + onSearch: handleApply, onClear: handleClear, child: Padding( padding: const EdgeInsets.all(16.0), @@ -283,28 +270,24 @@ class DriftSearchPage extends HookConsumerWidget { datePicked(DateFilterInputModel? selectedDate) { dateInputFilter.value = selectedDate; if (selectedDate == null) { - filter.value = filter.value.copyWith(date: SearchDateFilter()); - dateRangeCurrentFilterWidget.value = null; - unawaited(search()); + search(filter.value.copyWith(date: SearchDateFilter())); return; } final date = selectedDate.asDateTimeRange(); - - filter.value = filter.value.copyWith( - date: SearchDateFilter( - takenAfter: date.start, - takenBefore: date.end.add(const Duration(hours: 23, minutes: 59, seconds: 59)), - ), - ); - dateRangeCurrentFilterWidget.value = Text( selectedDate.asHumanReadable(context), style: context.textTheme.labelLarge, ); - - unawaited(search()); + search( + filter.value.copyWith( + date: SearchDateFilter( + takenAfter: date.start, + takenBefore: date.end.add(const Duration(hours: 23, minutes: 59, seconds: 59)), + ), + ), + ); } showDatePicker() async { @@ -357,11 +340,11 @@ class DriftSearchPage extends HookConsumerWidget { child: QuickDatePicker( currentInput: dateInputFilter.value, onRequestPicker: () { - context.pop(); + ContextHelper(context).pop(); showDatePicker(); }, onSelect: (date) { - context.pop(); + ContextHelper(context).pop(); datePicked(date); }, ), @@ -371,31 +354,32 @@ class DriftSearchPage extends HookConsumerWidget { // MEDIA PICKER showMediaTypePicker() { - handleOnSelected(AssetType assetType) { - filter.value = filter.value.copyWith(mediaType: assetType); + var mediaType = filter.value.mediaType; - mediaTypeCurrentFilterWidget.value = Text( - assetType == AssetType.image - ? 'image'.t(context: context) - : assetType == AssetType.video - ? 'video'.t(context: context) - : 'all'.t(context: context), - style: context.textTheme.labelLarge, - ); + handleOnSelected(AssetType assetType) { + mediaType = assetType; } handleClear() { - filter.value = filter.value.copyWith(mediaType: AssetType.other); - mediaTypeCurrentFilterWidget.value = null; - search(); + search(filter.value.copyWith(mediaType: AssetType.other)); + } + + handleApply() { + mediaTypeCurrentFilterWidget.value = mediaType != AssetType.other + ? Text( + mediaType == AssetType.image ? 'image'.t(context: context) : 'video'.t(context: context), + style: context.textTheme.labelLarge, + ) + : null; + search(filter.value.copyWith(mediaType: mediaType)); } showFilterBottomSheet( context: context, child: FilterBottomSheetScaffold( title: 'search_filter_media_type_title'.t(context: context), - onSearch: search, + onSearch: handleApply, onClear: handleClear, child: MediaTypePicker(onSelect: handleOnSelected, filter: filter.value.mediaType), ), @@ -404,19 +388,22 @@ class DriftSearchPage extends HookConsumerWidget { // STAR RATING PICKER showStarRatingPicker() { - handleOnSelected(SearchRatingFilter rating) { - filter.value = filter.value.copyWith(rating: rating); + var rating = filter.value.rating; - ratingCurrentFilterWidget.value = Text( - 'rating_count'.t(args: {'count': rating.rating!}), - style: context.textTheme.labelLarge, - ); + handleOnSelected(SearchRatingFilter value) { + rating = value; } handleClear() { - filter.value = filter.value.copyWith(rating: SearchRatingFilter(rating: null)); ratingCurrentFilterWidget.value = null; - search(); + search(filter.value.copyWith(rating: SearchRatingFilter(rating: null))); + } + + handleApply() { + ratingCurrentFilterWidget.value = rating.rating != null + ? Text('rating_count'.t(args: {'count': rating.rating!}), style: context.textTheme.labelLarge) + : null; + search(filter.value.copyWith(rating: rating)); } showFilterBottomSheet( @@ -424,7 +411,7 @@ class DriftSearchPage extends HookConsumerWidget { isScrollControlled: true, child: FilterBottomSheetScaffold( title: 'rating'.t(context: context), - onSearch: search, + onSearch: handleApply, onClear: handleClear, child: StarRatingPicker(onSelect: handleOnSelected, filter: filter.value.rating), ), @@ -433,79 +420,54 @@ class DriftSearchPage extends HookConsumerWidget { // DISPLAY OPTION showDisplayOptionPicker() { + var display = filter.value.display; + handleOnSelect(Map value) { - final filterText = []; - value.forEach((key, value) { - switch (key) { - case DisplayOption.notInAlbum: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isNotInAlbum: value)); - if (value) { - filterText.add('search_filter_display_option_not_in_album'.t(context: context)); - } - break; - case DisplayOption.archive: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isArchive: value)); - if (value) { - filterText.add('archive'.t(context: context)); - } - break; - case DisplayOption.favorite: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isFavorite: value)); - if (value) { - filterText.add('favorite'.t(context: context)); - } - break; - } - }); - - if (filterText.isEmpty) { - displayOptionCurrentFilterWidget.value = null; - return; - } - - displayOptionCurrentFilterWidget.value = Text(filterText.join(', '), style: context.textTheme.labelLarge); + display = display.copyWith( + isNotInAlbum: value[DisplayOption.notInAlbum], + isArchive: value[DisplayOption.archive], + isFavorite: value[DisplayOption.favorite], + ); } handleClear() { - filter.value = filter.value.copyWith( - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - ); - displayOptionCurrentFilterWidget.value = null; - search(); + search( + filter.value.copyWith( + display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), + ), + ); + } + + handleApply() { + final filterText = [ + if (display.isNotInAlbum) 'search_filter_display_option_not_in_album'.t(context: context), + if (display.isArchive) 'archive'.t(context: context), + if (display.isFavorite) 'favorite'.t(context: context), + ]; + displayOptionCurrentFilterWidget.value = filterText.isNotEmpty + ? Text(filterText.join(', '), style: context.textTheme.labelLarge) + : null; + search(filter.value.copyWith(display: display)); } showFilterBottomSheet( context: context, child: FilterBottomSheetScaffold( title: 'display_options'.t(context: context), - onSearch: search, + onSearch: handleApply, onClear: handleClear, child: DisplayOptionPicker(onSelect: handleOnSelect, filter: filter.value.display), ), ); } - handleTextSubmitted(String value) { - switch (textSearchType.value) { - case TextSearchType.context: - filter.value = filter.value.copyWith(filename: '', context: value, description: '', ocr: ''); - - break; - case TextSearchType.filename: - filter.value = filter.value.copyWith(filename: value, context: '', description: '', ocr: ''); - - break; - case TextSearchType.description: - filter.value = filter.value.copyWith(filename: '', context: '', description: value, ocr: ''); - break; - case TextSearchType.ocr: - filter.value = filter.value.copyWith(filename: '', context: '', description: '', ocr: value); - break; - } - - search(); - } + handleTextSubmitted(String value) => search(switch (textSearchType.value) { + TextSearchType.context => filter.value.copyWith(filename: '', context: value, description: '', ocr: ''), + TextSearchType.filename => filter.value.copyWith(filename: value, context: '', description: '', ocr: ''), + TextSearchType.description => filter.value.copyWith(filename: '', context: '', description: value, ocr: ''), + TextSearchType.ocr => filter.value.copyWith(filename: '', context: '', description: '', ocr: value), + }); IconData getSearchPrefixIcon() => switch (textSearchType.value) { TextSearchType.context => Icons.image_search_rounded, @@ -643,8 +605,10 @@ class DriftSearchPage extends HookConsumerWidget { hintText: searchHintText.value, key: const Key('search_text_field'), controller: textSearchController, - contentPadding: preFilter != null ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(8), - prefixIcon: preFilter != null ? null : Icon(getSearchPrefixIcon(), color: context.colorScheme.primary), + contentPadding: filter.value.assetId != null ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(8), + prefixIcon: filter.value.assetId != null + ? null + : Icon(getSearchPrefixIcon(), color: context.colorScheme.primary), onSubmitted: handleTextSubmitted, focusNode: ref.watch(searchInputFocusProvider), ), diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart index cad74ce658..564b02d884 100644 --- a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart @@ -1,10 +1,17 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/routing/router.dart'; class EditImageActionButton extends ConsumerWidget { @@ -14,13 +21,33 @@ class EditImageActionButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final currentAsset = ref.watch(assetViewerProvider.select((s) => s.currentAsset)); - onPress() { - if (currentAsset == null) { + Future editImage(List edits) async { + if (currentAsset == null || currentAsset.remoteId == null) { return; } - final image = Image(image: getFullImageProvider(currentAsset)); - context.pushRoute(DriftEditImageRoute(asset: currentAsset, image: image, isEdited: false)); + await ref.read(actionProvider.notifier).applyEdits(ActionSource.viewer, edits); + } + + Future onPress() async { + if (currentAsset == null || currentAsset.remoteId == null) { + return; + } + + final imageProvider = getFullImageProvider(currentAsset, edited: false); + + final image = Image(image: imageProvider); + final (edits, exifInfo) = await ( + ref.read(remoteAssetRepositoryProvider).getAssetEdits(currentAsset.remoteId!), + ref.read(remoteAssetRepositoryProvider).getExif(currentAsset.remoteId!), + ).wait; + + if (exifInfo == null) { + return; + } + + ref.read(editorStateProvider.notifier).init(edits, exifInfo); + await context.pushRoute(DriftEditImageRoute(image: image, applyEdits: editImage)); } return BaseActionButton( diff --git a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart index ba2491365d..07ace7e631 100644 --- a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.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/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -23,6 +25,12 @@ class FavoriteActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).favorite(source); if (source == ActionSource.viewer) { + if (result.success) { + final currentAsset = ref.read(assetViewerProvider).currentAsset; + if (currentAsset is RemoteAsset && !currentAsset.isFavorite) { + ref.read(assetViewerProvider.notifier).setAsset(currentAsset.copyWith(isFavorite: true)); + } + } return; } 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 96a7daa327..4cb973cca1 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 @@ -23,7 +23,7 @@ class LikeActivityActionButton extends ConsumerWidget { final asset = ref.watch(assetViewerProvider.select((s) => s.currentAsset)) as RemoteAsset?; final user = ref.watch(currentUserProvider); - final activities = ref.watch(albumActivityProvider(album?.id ?? "", asset?.id)); + final activities = ref.watch(albumActivityProvider((album?.id ?? "", asset?.id))); onTap(Activity? liked) async { if (user == null) { @@ -31,12 +31,12 @@ class LikeActivityActionButton extends ConsumerWidget { } if (liked != null) { - await ref.read(albumActivityProvider(album?.id ?? "", asset?.id).notifier).removeActivity(liked.id); + await ref.read(albumActivityProvider((album?.id ?? "", asset?.id)).notifier).removeActivity(liked.id); } else { - await ref.read(albumActivityProvider(album?.id ?? "", asset?.id).notifier).addLike(); + await ref.read(albumActivityProvider((album?.id ?? "", asset?.id)).notifier).addLike(); } - ref.invalidate(albumActivityProvider(album?.id ?? "", asset?.id)); + ref.invalidate(albumActivityProvider((album?.id ?? "", asset?.id))); } return activities.when( diff --git a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart index 530c3fd8d4..0acbbce613 100644 --- a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart'; @@ -24,20 +24,22 @@ class SimilarPhotosActionButton extends ConsumerWidget { } ref.invalidate(assetViewerProvider); - ref - .read(searchPreFilterProvider.notifier) - .setFilter( - SearchFilter( - assetId: assetId, - people: {}, - location: SearchLocationFilter(), - camera: SearchCameraFilter(), - date: SearchDateFilter(), - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - rating: SearchRatingFilter(), - mediaType: AssetType.image, - ), - ); + ref.invalidate(paginatedSearchProvider); + + ref.read(searchPreFilterProvider.notifier) + ..clear() + ..setFilter( + SearchFilter( + assetId: assetId, + people: {}, + location: SearchLocationFilter(), + camera: SearchCameraFilter(), + date: SearchDateFilter(), + display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), + rating: SearchRatingFilter(), + mediaType: AssetType.image, + ), + ); unawaited(context.navigateTo(const DriftSearchRoute())); } diff --git a/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart index ec5513e0a8..5e88735d9c 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.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/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -23,6 +25,12 @@ class UnFavoriteActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).unFavorite(source); if (source == ActionSource.viewer) { + if (result.success) { + final currentAsset = ref.read(assetViewerProvider).currentAsset; + if (currentAsset is RemoteAsset && currentAsset.isFavorite) { + ref.read(assetViewerProvider.notifier).setAsset(currentAsset.copyWith(isFavorite: false)); + } + } return; } diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 0c039847a4..c68a7273e0 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -17,9 +17,9 @@ import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.wi import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.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/asset_viewer/asset_viewer.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'; @@ -833,7 +833,7 @@ class CreateAlbumButton extends ConsumerWidget { // Invalidate using the asset's remote ID to refresh the "Appears in" list ref.invalidate(albumsContainingAssetProvider(asset.remoteId!)); - context.pop(); + ContextHelper(context).pop(); } return SliverPadding( diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart index 6c6f4a002c..32bbc915a1 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart @@ -6,10 +6,10 @@ import 'package:immich_mobile/domain/models/person.model.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/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/people/person_edit_name_modal.widget.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/utils/people.utils.dart'; @@ -73,7 +73,7 @@ class PeopleDetails extends ConsumerWidget { context.back(); return; } - context.pop(); + ContextHelper(context).pop(); context.pushRoute(DriftPersonRoute(person: person)); }, onNameTap: () => showNameEditModal(person), 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 4d8954d4ef..3308ae8295 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -65,13 +65,15 @@ class AssetViewer extends ConsumerStatefulWidget { static void setAsset(WidgetRef ref, BaseAsset asset) { ref.read(assetViewerProvider.notifier).reset(); + + // Hide controls by default for videos + if (asset.isVideo) ref.read(assetViewerProvider.notifier).setControls(false); + _setAsset(ref, asset); } static void _setAsset(WidgetRef ref, BaseAsset asset) { ref.read(assetViewerProvider.notifier).setAsset(asset); - // Hide controls by default for videos - if (asset.isVideo) ref.read(assetViewerProvider.notifier).setControls(false); } } 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 cc171f4490..cf7ffbd234 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -3,16 +3,18 @@ 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/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/add_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'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.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/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/utils/semver.dart'; import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; class ViewerBottomBar extends ConsumerWidget { @@ -30,6 +32,7 @@ class ViewerBottomBar extends ConsumerWidget { final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); final isInLockedView = ref.watch(inLockedViewProvider); + final serverInfo = ref.watch(serverInfoProvider); final originalTheme = context.themeData; @@ -38,7 +41,9 @@ class ViewerBottomBar extends ConsumerWidget { if (!isInLockedView) ...[ if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), - if (asset.type == AssetType.image) const EditImageActionButton(), + // edit sync was added in 2.6.0 + if (asset.isEditable && serverInfo.serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) + const EditImageActionButton(), if (asset.hasRemote) AddActionButton(originalTheme: originalTheme), if (isOwner) ...[ @@ -71,16 +76,13 @@ class ViewerBottomBar extends ConsumerWidget { ), child: SafeArea( top: false, - child: Padding( - padding: const EdgeInsets.only(top: 16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag), - if (!isReadonlyModeEnabled) - Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions), - ], - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag), + if (!isReadonlyModeEnabled) + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions), + ], ), ), ), diff --git a/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart index 64090dc5c2..62a439fe39 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart @@ -39,6 +39,16 @@ class _RatingBarState extends State { _currentRating = widget.initialRating; } + @override + void didUpdateWidget(covariant RatingBar oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.initialRating != widget.initialRating && _currentRating != widget.initialRating) { + setState(() { + _currentRating = widget.initialRating; + }); + } + } + void _updateRating(Offset localPosition, bool isRTL, {bool isTap = false}) { final totalWidth = widget.itemCount * widget.itemSize + (widget.itemCount - 1) * widget.starPadding; double dx = localPosition.dx; diff --git a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart index ae7dd85396..eb00b042a3 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart @@ -4,17 +4,17 @@ 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/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart'; import 'package:immich_mobile/providers/activity.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.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/user.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { const ViewerTopAppBar({super.key}); @@ -36,7 +36,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { final showingDetails = ref.watch(assetViewerProvider.select((state) => state.showingDetails)); if (album != null && album.isActivityEnabled && album.isShared && asset is RemoteAsset) { - ref.watch(albumActivityProvider(album.id, asset.id)); + ref.watch(albumActivityProvider((album.id, asset.id))); } final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); diff --git a/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart index 9f8216c4ed..c96e680966 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart @@ -10,20 +10,19 @@ class TrashBottomBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return SafeArea( - child: Align( - alignment: Alignment.bottomCenter, - child: SizedBox( - height: 64, - child: Container( - color: context.themeData.canvasColor, - child: const Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - DeleteTrashActionButton(source: ActionSource.timeline), - RestoreTrashActionButton(source: ActionSource.timeline), - ], - ), + return Align( + alignment: Alignment.bottomCenter, + child: Container( + color: context.themeData.canvasColor, + padding: const EdgeInsets.symmetric(vertical: 8), + child: const SafeArea( + top: false, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + DeleteTrashActionButton(source: ActionSource.timeline), + RestoreTrashActionButton(source: ActionSource.timeline), + ], ), ), ), diff --git a/mobile/lib/presentation/widgets/images/animated_image_stream_completer.dart b/mobile/lib/presentation/widgets/images/animated_image_stream_completer.dart index be4fbff8cf..796d30e992 100644 --- a/mobile/lib/presentation/widgets/images/animated_image_stream_completer.dart +++ b/mobile/lib/presentation/widgets/images/animated_image_stream_completer.dart @@ -3,24 +3,21 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart' show InformationCollector; import 'package:flutter/painting.dart'; +import 'package:immich_mobile/presentation/widgets/images/cache_aware_listener_tracker.mixin.dart'; /// A [MultiFrameImageStreamCompleter] with support for listener tracking /// which makes resource cleanup possible when no longer needed. /// Codec is disposed through the MultiFrameImageStreamCompleter's internals onDispose method -class AnimatedImageStreamCompleter extends MultiFrameImageStreamCompleter { - void Function()? _onLastListenerRemoved; - int _listenerCount = 0; - // True once any image or the codec has been provided. - // Until then the image cache holds one listener, so "last real listener gone" - // is _listenerCount == 1, not 0. - bool didProvideImage = false; - +class AnimatedImageStreamCompleter extends MultiFrameImageStreamCompleter with CacheAwareListenerTrackerMixin { AnimatedImageStreamCompleter._({ required super.codec, required super.scale, + required bool hadInitialImage, super.informationCollector, void Function()? onLastListenerRemoved, - }) : _onLastListenerRemoved = onLastListenerRemoved; + }) { + setupListenerTracking(hadInitialImage: hadInitialImage, onLastListenerRemoved: onLastListenerRemoved); + } factory AnimatedImageStreamCompleter({ required Stream stream, @@ -33,23 +30,21 @@ class AnimatedImageStreamCompleter extends MultiFrameImageStreamCompleter { final self = AnimatedImageStreamCompleter._( codec: codecCompleter.future, scale: scale, + hadInitialImage: initialImage != null, informationCollector: informationCollector, onLastListenerRemoved: onLastListenerRemoved, ); if (initialImage != null) { - self.didProvideImage = true; self.setImage(initialImage); } stream.listen( (item) { if (item is ImageInfo) { - self.didProvideImage = true; self.setImage(item); } else if (item is ui.Codec) { if (!codecCompleter.isCompleted) { - self.didProvideImage = true; codecCompleter.complete(item); } } @@ -70,27 +65,4 @@ class AnimatedImageStreamCompleter extends MultiFrameImageStreamCompleter { return self; } - - @override - void addListener(ImageStreamListener listener) { - super.addListener(listener); - _listenerCount++; - } - - @override - void removeListener(ImageStreamListener listener) { - super.removeListener(listener); - _listenerCount--; - - final bool onlyCacheListenerLeft = _listenerCount == 1 && !didProvideImage; - final bool noListenersAfterCodec = _listenerCount == 0 && didProvideImage; - - if (onlyCacheListenerLeft || noListenersAfterCodec) { - final onLastListenerRemoved = _onLastListenerRemoved; - if (onLastListenerRemoved != null) { - _onLastListenerRemoved = null; - onLastListenerRemoved(); - } - } - } } diff --git a/mobile/lib/presentation/widgets/images/cache_aware_listener_tracker.mixin.dart b/mobile/lib/presentation/widgets/images/cache_aware_listener_tracker.mixin.dart new file mode 100644 index 0000000000..e63d5c4cfc --- /dev/null +++ b/mobile/lib/presentation/widgets/images/cache_aware_listener_tracker.mixin.dart @@ -0,0 +1,84 @@ +import 'package:flutter/painting.dart'; + +/// Tracks listeners on an [ImageStreamCompleter] to safely cancel in-flight +/// network requests without interfering with [ImageCache] internals. +/// +/// ### Problem +/// Cancelling fetches when the listener count drops to 1 (cache only) or 0 +/// is unsafe due to three framework behaviours: +/// +/// 1. **Memory-pressure eviction** — `ImageCache.clear()` removes the cache +/// listener while UI widgets still need the image. A count-based check +/// would cancel the active fetch, leaving the UI with no image. +/// 2. **Synchronous detach during `putIfAbsent`** — When an `initialImage` +/// is provided, the cache attaches, receives the frame, and detaches +/// synchronously *before* the UI widget can attach. Count reaches 0 and +/// would trigger a false cancel. +/// 3. **Listener misidentification** — After the cache detaches (via 1 or 2), +/// the next UI listener could be mistaken for the cache listener, causing +/// incorrect cancellations when that widget is disposed. +/// +/// ### Solution: First-Listener Heuristic +/// The cache is always the first listener attached (via `putIfAbsent`). This +/// mixin records that identity once and uses it for all subsequent decisions: +/// +/// * **Identity locking** — The first listener is assumed to be the cache. +/// Once identified, `_hasIdentifiedCacheListener` prevents reassignment. +/// * **Targeted cancellation** — Cancel only when the identified cache +/// listener is the sole remaining listener and no image has been delivered. +/// * **Sync-removal bypass** — When `hadInitialImage` is set, the first +/// synchronous removal of the cache listener is ignored so the fetch +/// survives until the UI attaches. +mixin CacheAwareListenerTrackerMixin on ImageStreamCompleter { + void Function()? _onLastListenerRemoved; + int _listenerCount = 0; + bool _hadInitialImage = false; + bool _hasIgnoredFirstSyncRemoval = false; + ImageStreamListener? _cacheListener; + bool _hasIdentifiedCacheListener = false; + + /// Initializes the tracking state. Must be called in the subclass constructor. + void setupListenerTracking({required bool hadInitialImage, void Function()? onLastListenerRemoved}) { + _hadInitialImage = hadInitialImage; + _onLastListenerRemoved = onLastListenerRemoved; + } + + @override + void addListener(ImageStreamListener listener) { + if (!_hasIdentifiedCacheListener) { + _hasIdentifiedCacheListener = true; + _cacheListener = listener; + } + + _listenerCount++; + super.addListener(listener); + } + + @override + void removeListener(ImageStreamListener listener) { + super.removeListener(listener); + _listenerCount--; + + final bool isCacheListener = listener == _cacheListener; + if (isCacheListener) { + _cacheListener = null; + } + + if (_hadInitialImage && !_hasIgnoredFirstSyncRemoval && isCacheListener) { + _hasIgnoredFirstSyncRemoval = true; + return; + } + + final bool onlyCacheListenerLeft = _listenerCount == 1 && _cacheListener != null; + + final bool completelyAbandoned = _listenerCount == 0; + + if (onlyCacheListenerLeft || completelyAbandoned) { + final onLastListenerRemoved = _onLastListenerRemoved; + if (onLastListenerRemoved != null) { + _onLastListenerRemoved = null; + onLastListenerRemoved(); + } + } + } +} diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index bf29f9482f..ea416d9d71 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -19,6 +19,7 @@ mixin CancellableImageProviderMixin on CancellableImageProvide static final _log = Logger('CancellableImageProviderMixin'); bool isCancelled = false; + bool isFinished = false; ImageRequest? request; CancelableOperation? cachedOperation; @@ -50,24 +51,26 @@ mixin CancellableImageProviderMixin on CancellableImageProvide return null; } - Stream loadRequest(ImageRequest request, ImageDecoderCallback decode, {bool evictOnError = true}) async* { + Stream loadRequest(ImageRequest request, ImageDecoderCallback decode, {required bool isFinal}) async* { if (isCancelled) { this.request = null; - PaintingBinding.instance.imageCache.evict(this); return; } try { final image = await request.load(decode); - if ((image == null && evictOnError) || isCancelled) { - PaintingBinding.instance.imageCache.evict(this); - return; - } else if (image == null) { + if (isCancelled || image == null) { + image?.dispose(); return; } + isFinished = isFinal; yield image; } catch (e, stack) { - if (evictOnError) { + if (isCancelled) { + return; + } + if (isFinal) { + isFinished = true; PaintingBinding.instance.imageCache.evict(this); rethrow; } @@ -77,24 +80,27 @@ mixin CancellableImageProviderMixin on CancellableImageProvide } } - Future loadCodecRequest(ImageRequest request) async { + Future loadCodecRequest(ImageRequest request, {required bool isFinal}) async { if (isCancelled) { this.request = null; - PaintingBinding.instance.imageCache.evict(this); return null; } try { final codec = await request.loadCodec(); - if (codec == null || isCancelled) { + if (isCancelled || codec == null) { codec?.dispose(); - PaintingBinding.instance.imageCache.evict(this); return null; } + isFinished = isFinal; return codec; } catch (e) { - PaintingBinding.instance.imageCache.evict(this); - rethrow; + if (isFinal) { + isFinished = true; + PaintingBinding.instance.imageCache.evict(this); + rethrow; + } + return null; } finally { this.request = null; } @@ -121,6 +127,8 @@ mixin CancellableImageProviderMixin on CancellableImageProvide @override void cancel() { isCancelled = true; + final hasActiveWork = !isFinished; + final request = this.request; if (request != null) { this.request = null; @@ -132,10 +140,14 @@ mixin CancellableImageProviderMixin on CancellableImageProvide cachedOperation = null; operation.cancel(); } + + if (hasActiveWork) { + PaintingBinding.instance.imageCache.evict(this); + } } } -ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920)}) { +ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920), bool edited = true}) { // Create new provider and cache it final ImageProvider provider; if (_shouldUseLocalAsset(asset)) { @@ -158,13 +170,14 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080 thumbhash: thumbhash, assetType: asset.type, isAnimated: asset.isAnimatedImage, + edited: edited, ); } return provider; } -ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution}) { +ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution, bool edited = true}) { if (_shouldUseLocalAsset(asset)) { final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; return LocalThumbProvider(id: id, size: size, assetType: asset.type); @@ -172,7 +185,7 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId; final thumbhash = asset is RemoteAsset ? asset.thumbHash ?? "" : ""; - return assetId != null ? RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: thumbhash) : null; + return assetId != null ? RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: thumbhash, edited: edited) : null; } bool _shouldUseLocalAsset(BaseAsset asset) => diff --git a/mobile/lib/presentation/widgets/images/local_image_provider.dart b/mobile/lib/presentation/widgets/images/local_image_provider.dart index 1ed2c361ff..d29a1cd56d 100644 --- a/mobile/lib/presentation/widgets/images/local_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -36,7 +36,7 @@ class LocalThumbProvider extends CancellableImageProvider Stream _codec(LocalThumbProvider key, ImageDecoderCallback decode) { final request = this.request = LocalImageRequest(localId: key.id, size: key.size, assetType: key.assetType); - return loadRequest(request, decode); + return loadRequest(request, decode, isFinal: true); } @override @@ -100,37 +100,35 @@ class LocalFullImageProvider extends CancellableImageProvider _animatedCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* { yield* initialImageStream(); if (isCancelled) { - PaintingBinding.instance.imageCache.evict(this); return; } @@ -140,17 +138,17 @@ class LocalFullImageProvider extends CancellableImageProvider with CancellableImageProviderMixin { final String url; + final bool edited; - RemoteImageProvider({required this.url}); + RemoteImageProvider({required this.url, this.edited = true}); - RemoteImageProvider.thumbnail({required String assetId, required String thumbhash}) - : url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash); + RemoteImageProvider.thumbnail({required String assetId, required String thumbhash, this.edited = true}) + : url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash, edited: edited); @override Future obtainKey(ImageConfiguration configuration) { @@ -38,20 +39,20 @@ class RemoteImageProvider extends CancellableImageProvider Stream _codec(RemoteImageProvider key, ImageDecoderCallback decode) { final request = this.request = RemoteImageRequest(uri: key.url); - return loadRequest(request, decode); + return loadRequest(request, decode, isFinal: true); } @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other is RemoteImageProvider) { - return url == other.url; + return url == other.url && edited == other.edited; } return false; } @override - int get hashCode => url.hashCode; + int get hashCode => url.hashCode ^ edited.hashCode; } class RemoteFullImageProvider extends CancellableImageProvider @@ -60,12 +61,14 @@ class RemoteFullImageProvider extends CancellableImageProvider [ DiagnosticsProperty('Image provider', this), DiagnosticsProperty('Asset Id', key.assetId), @@ -105,51 +110,64 @@ class RemoteFullImageProvider extends CancellableImageProvider _animatedCodec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* { yield* initialImageStream(); if (isCancelled) { - PaintingBinding.instance.imageCache.evict(this); return; } final previewRequest = request = RemoteImageRequest( - uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash), + uri: getThumbnailUrlForRemoteId( + key.assetId, + type: AssetMediaSize.preview, + thumbhash: key.thumbhash, + edited: key.edited, + ), ); - yield* loadRequest(previewRequest, decode, evictOnError: false); + yield* loadRequest(previewRequest, decode, isFinal: false); if (isCancelled) { - PaintingBinding.instance.imageCache.evict(this); return; } // always try original for animated, since previews don't support animation - final originalRequest = request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId)); - final codec = await loadCodecRequest(originalRequest); + final originalRequest = request = RemoteImageRequest( + uri: getOriginalUrlForRemoteId(key.assetId, edited: key.edited), + ); + final codec = await loadCodecRequest(originalRequest, isFinal: true); if (codec == null) { + if (isCancelled) { + return; + } throw StateError('Failed to load animated codec for asset ${key.assetId}'); } yield codec; @@ -159,12 +177,15 @@ class RemoteFullImageProvider extends CancellableImageProvider assetId.hashCode ^ thumbhash.hashCode ^ isAnimated.hashCode; + int get hashCode => assetId.hashCode ^ thumbhash.hashCode ^ isAnimated.hashCode ^ edited.hashCode; } diff --git a/mobile/lib/presentation/widgets/images/thumb_hash_provider.dart b/mobile/lib/presentation/widgets/images/thumb_hash_provider.dart index 7076febe3b..02f957a5d9 100644 --- a/mobile/lib/presentation/widgets/images/thumb_hash_provider.dart +++ b/mobile/lib/presentation/widgets/images/thumb_hash_provider.dart @@ -22,7 +22,7 @@ class ThumbHashProvider extends CancellableImageProvider Stream _loadCodec(ThumbHashProvider key, ImageDecoderCallback decode) { final request = this.request = ThumbhashImageRequest(thumbhash: key.thumbHash); - return loadRequest(request, decode); + return loadRequest(request, decode, isFinal: true); } @override diff --git a/mobile/lib/presentation/widgets/map/map.widget.dart b/mobile/lib/presentation/widgets/map/map.widget.dart index 72f4e8bda6..3f406dd551 100644 --- a/mobile/lib/presentation/widgets/map/map.widget.dart +++ b/mobile/lib/presentation/widgets/map/map.widget.dart @@ -132,7 +132,7 @@ class _DriftMapState extends ConsumerState { // If we continue to update bounds, the map-scoped timeline service gets recreated and the previous one disposed, // which can invalidate the TimelineService instance that was passed into AssetViewerRoute (causing "loading forever"). final currentRoute = ref.read(currentRouteNameProvider); - if (currentRoute == AssetViewerRoute.name || currentRoute == GalleryViewerRoute.name) { + if (currentRoute == AssetViewerRoute.name) { return; } diff --git a/mobile/lib/providers/activity.provider.dart b/mobile/lib/providers/activity.provider.dart index 5e0e71d85d..b2cdbcf18c 100644 --- a/mobile/lib/providers/activity.provider.dart +++ b/mobile/lib/providers/activity.provider.dart @@ -1,17 +1,22 @@ import 'package:collection/collection.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/activities/activity.model.dart'; import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'activity.provider.g.dart'; // ignore: unintended_html_in_doc_comment /// Maintains the current list of all activities for -@riverpod -class AlbumActivity extends _$AlbumActivity { + +final albumActivityProvider = AsyncNotifierProvider.autoDispose + .family, (String albumId, String? assetId)>(AlbumActivity.new); + +class AlbumActivity extends AutoDisposeFamilyAsyncNotifier, (String albumId, String? assetId)> { + late String albumId; + late String? assetId; + @override - Future> build(String albumId, [String? assetId]) async { + Future> build((String albumId, String? assetId) args) async { + albumId = args.$1; + assetId = args.$2; return ref.watch(activityServiceProvider).getAllActivities(albumId, assetId: assetId); } @@ -23,14 +28,7 @@ class AlbumActivity extends _$AlbumActivity { } if (assetId != null) { - ref.read(albumActivityProvider(albumId).notifier)._removeFromState(id); - } - - if (removedActivity.type == ActivityType.comment) { - ref.watch(activityStatisticsProvider(albumId, assetId).notifier).removeActivity(); - if (assetId != null) { - ref.watch(activityStatisticsProvider(albumId).notifier).removeActivity(); - } + ref.read(albumActivityProvider((albumId, assetId)).notifier)._removeFromState(id); } } } @@ -40,7 +38,7 @@ class AlbumActivity extends _$AlbumActivity { if (activity.hasValue) { _addToState(activity.requireValue); if (assetId != null) { - ref.read(albumActivityProvider(albumId).notifier)._addToState(activity.requireValue); + ref.read(albumActivityProvider((albumId, assetId)).notifier)._addToState(activity.requireValue); } } } @@ -53,13 +51,7 @@ class AlbumActivity extends _$AlbumActivity { if (activity.hasValue) { _addToState(activity.requireValue); if (assetId != null) { - ref.read(albumActivityProvider(albumId).notifier)._addToState(activity.requireValue); - } - ref.watch(activityStatisticsProvider(albumId, assetId).notifier).addActivity(); - // The previous addActivity call would increase the count of an asset if assetId != null - // To also increase the activity count of the album, calling it once again with assetId set to null - if (assetId != null) { - ref.watch(activityStatisticsProvider(albumId).notifier).addActivity(); + ref.read(albumActivityProvider((albumId, assetId)).notifier)._addToState(activity.requireValue); } } } @@ -87,6 +79,3 @@ class AlbumActivity extends _$AlbumActivity { return activity; } } - -/// Mock class for testing -abstract class AlbumActivityInternal extends _$AlbumActivity {} diff --git a/mobile/lib/providers/activity.provider.g.dart b/mobile/lib/providers/activity.provider.g.dart deleted file mode 100644 index 6ca99e4f72..0000000000 --- a/mobile/lib/providers/activity.provider.g.dart +++ /dev/null @@ -1,194 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'activity.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$albumActivityHash() => r'154e8ae98da3efc142369eae46d4005468fd67da'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -abstract class _$AlbumActivity - extends BuildlessAutoDisposeAsyncNotifier> { - late final String albumId; - late final String? assetId; - - FutureOr> build(String albumId, [String? assetId]); -} - -/// Maintains the current list of all activities for -/// -/// Copied from [AlbumActivity]. -@ProviderFor(AlbumActivity) -const albumActivityProvider = AlbumActivityFamily(); - -/// Maintains the current list of all activities for -/// -/// Copied from [AlbumActivity]. -class AlbumActivityFamily extends Family>> { - /// Maintains the current list of all activities for - /// - /// Copied from [AlbumActivity]. - const AlbumActivityFamily(); - - /// Maintains the current list of all activities for - /// - /// Copied from [AlbumActivity]. - AlbumActivityProvider call(String albumId, [String? assetId]) { - return AlbumActivityProvider(albumId, assetId); - } - - @override - AlbumActivityProvider getProviderOverride( - covariant AlbumActivityProvider provider, - ) { - return call(provider.albumId, provider.assetId); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'albumActivityProvider'; -} - -/// Maintains the current list of all activities for -/// -/// Copied from [AlbumActivity]. -class AlbumActivityProvider - extends - AutoDisposeAsyncNotifierProviderImpl> { - /// Maintains the current list of all activities for - /// - /// Copied from [AlbumActivity]. - AlbumActivityProvider(String albumId, [String? assetId]) - : this._internal( - () => AlbumActivity() - ..albumId = albumId - ..assetId = assetId, - from: albumActivityProvider, - name: r'albumActivityProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$albumActivityHash, - dependencies: AlbumActivityFamily._dependencies, - allTransitiveDependencies: - AlbumActivityFamily._allTransitiveDependencies, - albumId: albumId, - assetId: assetId, - ); - - AlbumActivityProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.albumId, - required this.assetId, - }) : super.internal(); - - final String albumId; - final String? assetId; - - @override - FutureOr> runNotifierBuild(covariant AlbumActivity notifier) { - return notifier.build(albumId, assetId); - } - - @override - Override overrideWith(AlbumActivity Function() create) { - return ProviderOverride( - origin: this, - override: AlbumActivityProvider._internal( - () => create() - ..albumId = albumId - ..assetId = assetId, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - albumId: albumId, - assetId: assetId, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement> - createElement() { - return _AlbumActivityProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is AlbumActivityProvider && - other.albumId == albumId && - other.assetId == assetId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, albumId.hashCode); - hash = _SystemHash.combine(hash, assetId.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin AlbumActivityRef on AutoDisposeAsyncNotifierProviderRef> { - /// The parameter `albumId` of this provider. - String get albumId; - - /// The parameter `assetId` of this provider. - String? get assetId; -} - -class _AlbumActivityProviderElement - extends - AutoDisposeAsyncNotifierProviderElement> - with AlbumActivityRef { - _AlbumActivityProviderElement(super.provider); - - @override - String get albumId => (origin as AlbumActivityProvider).albumId; - @override - String? get assetId => (origin as AlbumActivityProvider).assetId; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/activity_service.provider.dart b/mobile/lib/providers/activity_service.provider.dart index f17617bced..3be6c6b234 100644 --- a/mobile/lib/providers/activity_service.provider.dart +++ b/mobile/lib/providers/activity_service.provider.dart @@ -3,13 +3,11 @@ import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/repositories/activity_api.repository.dart'; import 'package:immich_mobile/services/activity.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'activity_service.provider.g.dart'; - -@riverpod -ActivityService activityService(Ref ref) => ActivityService( - ref.watch(activityApiRepositoryProvider), - ref.watch(timelineFactoryProvider), - ref.watch(assetServiceProvider), -); +final activityServiceProvider = Provider.autoDispose((ref) { + return ActivityService( + ref.watch(activityApiRepositoryProvider), + ref.watch(timelineFactoryProvider), + ref.watch(assetServiceProvider), + ); +}); diff --git a/mobile/lib/providers/activity_service.provider.g.dart b/mobile/lib/providers/activity_service.provider.g.dart deleted file mode 100644 index 4641738fc4..0000000000 --- a/mobile/lib/providers/activity_service.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'activity_service.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$activityServiceHash() => r'3ce0eb33948138057cc63f07a7598047b99e7599'; - -/// See also [activityService]. -@ProviderFor(activityService) -final activityServiceProvider = AutoDisposeProvider.internal( - activityService, - name: r'activityServiceProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$activityServiceHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef ActivityServiceRef = AutoDisposeProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/activity_statistics.provider.dart b/mobile/lib/providers/activity_statistics.provider.dart deleted file mode 100644 index 96d2633d1b..0000000000 --- a/mobile/lib/providers/activity_statistics.provider.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'activity_statistics.provider.g.dart'; - -// ignore: unintended_html_in_doc_comment -/// Maintains the current number of comments by -@riverpod -class ActivityStatistics extends _$ActivityStatistics { - @override - int build(String albumId, [String? assetId]) { - ref.watch(activityServiceProvider).getStatistics(albumId, assetId: assetId).then((stats) => state = stats.comments); - return 0; - } - - void addActivity() => state = state + 1; - - void removeActivity() => state = state - 1; -} - -/// Mock class for testing -abstract class ActivityStatisticsInternal extends _$ActivityStatistics {} diff --git a/mobile/lib/providers/activity_statistics.provider.g.dart b/mobile/lib/providers/activity_statistics.provider.g.dart deleted file mode 100644 index 83d887f6dc..0000000000 --- a/mobile/lib/providers/activity_statistics.provider.g.dart +++ /dev/null @@ -1,191 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'activity_statistics.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$activityStatisticsHash() => - r'1f43f0bcb11c754ca3cb586a13570db25023b9a8'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -abstract class _$ActivityStatistics extends BuildlessAutoDisposeNotifier { - late final String albumId; - late final String? assetId; - - int build(String albumId, [String? assetId]); -} - -/// Maintains the current number of comments by -/// -/// Copied from [ActivityStatistics]. -@ProviderFor(ActivityStatistics) -const activityStatisticsProvider = ActivityStatisticsFamily(); - -/// Maintains the current number of comments by -/// -/// Copied from [ActivityStatistics]. -class ActivityStatisticsFamily extends Family { - /// Maintains the current number of comments by - /// - /// Copied from [ActivityStatistics]. - const ActivityStatisticsFamily(); - - /// Maintains the current number of comments by - /// - /// Copied from [ActivityStatistics]. - ActivityStatisticsProvider call(String albumId, [String? assetId]) { - return ActivityStatisticsProvider(albumId, assetId); - } - - @override - ActivityStatisticsProvider getProviderOverride( - covariant ActivityStatisticsProvider provider, - ) { - return call(provider.albumId, provider.assetId); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'activityStatisticsProvider'; -} - -/// Maintains the current number of comments by -/// -/// Copied from [ActivityStatistics]. -class ActivityStatisticsProvider - extends AutoDisposeNotifierProviderImpl { - /// Maintains the current number of comments by - /// - /// Copied from [ActivityStatistics]. - ActivityStatisticsProvider(String albumId, [String? assetId]) - : this._internal( - () => ActivityStatistics() - ..albumId = albumId - ..assetId = assetId, - from: activityStatisticsProvider, - name: r'activityStatisticsProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$activityStatisticsHash, - dependencies: ActivityStatisticsFamily._dependencies, - allTransitiveDependencies: - ActivityStatisticsFamily._allTransitiveDependencies, - albumId: albumId, - assetId: assetId, - ); - - ActivityStatisticsProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.albumId, - required this.assetId, - }) : super.internal(); - - final String albumId; - final String? assetId; - - @override - int runNotifierBuild(covariant ActivityStatistics notifier) { - return notifier.build(albumId, assetId); - } - - @override - Override overrideWith(ActivityStatistics Function() create) { - return ProviderOverride( - origin: this, - override: ActivityStatisticsProvider._internal( - () => create() - ..albumId = albumId - ..assetId = assetId, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - albumId: albumId, - assetId: assetId, - ), - ); - } - - @override - AutoDisposeNotifierProviderElement createElement() { - return _ActivityStatisticsProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is ActivityStatisticsProvider && - other.albumId == albumId && - other.assetId == assetId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, albumId.hashCode); - hash = _SystemHash.combine(hash, assetId.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin ActivityStatisticsRef on AutoDisposeNotifierProviderRef { - /// The parameter `albumId` of this provider. - String get albumId; - - /// The parameter `assetId` of this provider. - String? get assetId; -} - -class _ActivityStatisticsProviderElement - extends AutoDisposeNotifierProviderElement - with ActivityStatisticsRef { - _ActivityStatisticsProviderElement(super.provider); - - @override - String get albumId => (origin as ActivityStatisticsProvider).albumId; - @override - String? get assetId => (origin as ActivityStatisticsProvider).assetId; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart deleted file mode 100644 index 35634d77c8..0000000000 --- a/mobile/lib/providers/album/album.provider.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'dart:async'; - -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/services/album.service.dart'; - -final isRefreshingRemoteAlbumProvider = StateProvider((ref) => false); - -class AlbumNotifier extends StateNotifier> { - AlbumNotifier(this.albumService, this.ref) : super([]) { - albumService.getAllRemoteAlbums().then((value) { - if (mounted) { - state = value; - } - }); - - _streamSub = albumService.watchRemoteAlbums().listen((data) => state = data); - } - - final AlbumService albumService; - final Ref ref; - late final StreamSubscription> _streamSub; - - Future refreshRemoteAlbums() async { - ref.read(isRefreshingRemoteAlbumProvider.notifier).state = true; - await albumService.refreshRemoteAlbums(); - ref.read(isRefreshingRemoteAlbumProvider.notifier).state = false; - } - - Future refreshDeviceAlbums() => albumService.refreshDeviceAlbums(); - - Future deleteAlbum(Album album) => albumService.deleteAlbum(album); - - Future createAlbum(String albumTitle, Set assets) => albumService.createAlbum(albumTitle, assets, []); - - Future getAlbumByName(String albumName, {bool? remote, bool? shared, bool? owner}) => - albumService.getAlbumByName(albumName, remote: remote, shared: shared, owner: owner); - - /// Create an album on the server with the same name as the selected album for backup - /// First this will check if the album already exists on the server with name - /// If it does not exist, it will create the album on the server - Future createSyncAlbum(String albumName) async { - final album = await getAlbumByName(albumName, remote: true, owner: true); - if (album != null) { - return; - } - - await createAlbum(albumName, {}); - } - - Future leaveAlbum(Album album) async { - var res = await albumService.leaveAlbum(album); - - if (res) { - await deleteAlbum(album); - return true; - } else { - return false; - } - } - - void searchAlbums(String searchTerm, QuickFilterMode filterMode) async { - state = await albumService.search(searchTerm, filterMode); - } - - Future addUsers(Album album, List userIds) async { - await albumService.addUsers(album, userIds); - } - - Future removeUser(Album album, UserDto user) async { - final isRemoved = await albumService.removeUser(album, user); - - if (isRemoved && album.sharedUsers.isEmpty) { - state = state.where((element) => element.id != album.id).toList(); - } - - return isRemoved; - } - - Future addAssets(Album album, Iterable assets) async { - await albumService.addAssets(album, assets); - } - - Future removeAsset(Album album, Iterable assets) async { - return await albumService.removeAsset(album, assets); - } - - Future setActivitystatus(Album album, bool enabled) { - return albumService.setActivityStatus(album, enabled); - } - - Future toggleSortOrder(Album album) { - final order = album.sortOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc; - - return albumService.updateSortOrder(album, order); - } - - @override - void dispose() { - _streamSub.cancel(); - super.dispose(); - } -} - -final albumProvider = StateNotifierProvider.autoDispose>((ref) { - return AlbumNotifier(ref.watch(albumServiceProvider), ref); -}); - -final albumWatcher = StreamProvider.autoDispose.family((ref, id) async* { - final albumService = ref.watch(albumServiceProvider); - - final album = await albumService.getAlbumById(id); - if (album != null) { - yield album; - } - - await for (final album in albumService.watchAlbum(id)) { - if (album != null) { - yield album; - } - } -}); - -class LocalAlbumsNotifier extends StateNotifier> { - LocalAlbumsNotifier(this.albumService) : super([]) { - albumService.getAllLocalAlbums().then((value) { - if (mounted) { - state = value; - } - }); - - _streamSub = albumService.watchLocalAlbums().listen((data) => state = data); - } - - final AlbumService albumService; - late final StreamSubscription> _streamSub; - - @override - void dispose() { - _streamSub.cancel(); - super.dispose(); - } -} - -final localAlbumsProvider = StateNotifierProvider.autoDispose>((ref) { - return LocalAlbumsNotifier(ref.watch(albumServiceProvider)); -}); diff --git a/mobile/lib/providers/album/album_sort_by_options.provider.dart b/mobile/lib/providers/album/album_sort_by_options.provider.dart index c969dbd37d..ec4ae71d03 100644 --- a/mobile/lib/providers/album/album_sort_by_options.provider.dart +++ b/mobile/lib/providers/album/album_sort_by_options.provider.dart @@ -1,119 +1,19 @@ -import 'package:collection/collection.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'album_sort_by_options.provider.g.dart'; - -typedef AlbumSortFn = List Function(List albums, bool isReverse); - -class _AlbumSortHandlers { - const _AlbumSortHandlers._(); - - static const AlbumSortFn created = _sortByCreated; - static List _sortByCreated(List albums, bool isReverse) { - final sorted = albums.sortedBy((album) => album.createdAt); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn title = _sortByTitle; - static List _sortByTitle(List albums, bool isReverse) { - final sorted = albums.sortedBy((album) => album.name); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn lastModified = _sortByLastModified; - static List _sortByLastModified(List albums, bool isReverse) { - final sorted = albums.sortedBy((album) => album.modifiedAt); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn assetCount = _sortByAssetCount; - static List _sortByAssetCount(List albums, bool isReverse) { - final sorted = albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount)); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn mostRecent = _sortByMostRecent; - static List _sortByMostRecent(List albums, bool isReverse) { - final sorted = albums.sorted((a, b) { - if (a.endDate == null && b.endDate == null) { - return 0; - } - - if (a.endDate == null) { - // Put nulls at the end for recent sorting - return 1; - } - - if (b.endDate == null) { - return -1; - } - - // Sort by descending recent date - return b.endDate!.compareTo(a.endDate!); - }); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn mostOldest = _sortByMostOldest; - static List _sortByMostOldest(List albums, bool isReverse) { - final sorted = albums.sorted((a, b) { - if (a.startDate != null && b.startDate != null) { - return a.startDate!.compareTo(b.startDate!); - } - if (a.startDate == null) return 1; - if (b.startDate == null) return -1; - return 0; - }); - return (isReverse ? sorted.reversed : sorted).toList(); - } -} // Store index allows us to re-arrange the values without affecting the saved prefs enum AlbumSortMode { - title(1, "library_page_sort_title", _AlbumSortHandlers.title, SortOrder.asc), - assetCount(4, "library_page_sort_asset_count", _AlbumSortHandlers.assetCount, SortOrder.desc), - lastModified(3, "library_page_sort_last_modified", _AlbumSortHandlers.lastModified, SortOrder.desc), - created(0, "library_page_sort_created", _AlbumSortHandlers.created, SortOrder.desc), - mostRecent(2, "sort_recent", _AlbumSortHandlers.mostRecent, SortOrder.desc), - mostOldest(5, "sort_oldest", _AlbumSortHandlers.mostOldest, SortOrder.asc); + title(1, "library_page_sort_title", SortOrder.asc), + assetCount(4, "library_page_sort_asset_count", SortOrder.desc), + lastModified(3, "library_page_sort_last_modified", SortOrder.desc), + created(0, "library_page_sort_created", SortOrder.desc), + mostRecent(2, "sort_recent", SortOrder.desc), + mostOldest(5, "sort_oldest", SortOrder.asc); final int storeIndex; final String label; - final AlbumSortFn sortFn; final SortOrder defaultOrder; - const AlbumSortMode(this.storeIndex, this.label, this.sortFn, this.defaultOrder); + const AlbumSortMode(this.storeIndex, this.label, this.defaultOrder); SortOrder effectiveOrder(bool isReverse) => isReverse ? defaultOrder.reverse() : defaultOrder; } - -@riverpod -class AlbumSortByOptions extends _$AlbumSortByOptions { - @override - AlbumSortMode build() { - final sortOpt = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.selectedAlbumSortOrder); - return AlbumSortMode.values.firstWhere((e) => e.storeIndex == sortOpt, orElse: () => AlbumSortMode.title); - } - - void changeSortMode(AlbumSortMode sortOption) { - state = sortOption; - ref.watch(appSettingsServiceProvider).setSetting(AppSettingsEnum.selectedAlbumSortOrder, sortOption.storeIndex); - } -} - -@riverpod -class AlbumSortOrder extends _$AlbumSortOrder { - @override - bool build() { - return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.selectedAlbumSortReverse); - } - - void changeSortDirection(bool isReverse) { - state = isReverse; - ref.watch(appSettingsServiceProvider).setSetting(AppSettingsEnum.selectedAlbumSortReverse, isReverse); - } -} diff --git a/mobile/lib/providers/album/album_sort_by_options.provider.g.dart b/mobile/lib/providers/album/album_sort_by_options.provider.g.dart deleted file mode 100644 index 750329c9d5..0000000000 --- a/mobile/lib/providers/album/album_sort_by_options.provider.g.dart +++ /dev/null @@ -1,43 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'album_sort_by_options.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$albumSortByOptionsHash() => - r'dd8da5e730af555de1b86c3b157b6c93183523ac'; - -/// See also [AlbumSortByOptions]. -@ProviderFor(AlbumSortByOptions) -final albumSortByOptionsProvider = - AutoDisposeNotifierProvider.internal( - AlbumSortByOptions.new, - name: r'albumSortByOptionsProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$albumSortByOptionsHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$AlbumSortByOptions = AutoDisposeNotifier; -String _$albumSortOrderHash() => r'573dea45b4519e69386fc7104c72522e35713440'; - -/// See also [AlbumSortOrder]. -@ProviderFor(AlbumSortOrder) -final albumSortOrderProvider = - AutoDisposeNotifierProvider.internal( - AlbumSortOrder.new, - name: r'albumSortOrderProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$albumSortOrderHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$AlbumSortOrder = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/album/album_viewer.provider.dart b/mobile/lib/providers/album/album_viewer.provider.dart deleted file mode 100644 index f4ce047464..0000000000 --- a/mobile/lib/providers/album/album_viewer.provider.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/models/albums/album_viewer_page_state.model.dart'; -import 'package:immich_mobile/services/album.service.dart'; - -class AlbumViewerNotifier extends StateNotifier { - AlbumViewerNotifier(this.ref) - : super(const AlbumViewerPageState(editTitleText: "", isEditAlbum: false, editDescriptionText: "")); - - final Ref ref; - - void enableEditAlbum() { - state = state.copyWith(isEditAlbum: true); - } - - void disableEditAlbum() { - state = state.copyWith(isEditAlbum: false); - } - - void setEditTitleText(String newTitle) { - state = state.copyWith(editTitleText: newTitle); - } - - void setEditDescriptionText(String newDescription) { - state = state.copyWith(editDescriptionText: newDescription); - } - - void remoteEditTitleText() { - state = state.copyWith(editTitleText: ""); - } - - void remoteEditDescriptionText() { - state = state.copyWith(editDescriptionText: ""); - } - - void resetState() { - state = state.copyWith(editTitleText: "", isEditAlbum: false, editDescriptionText: ""); - } - - Future changeAlbumTitle(Album album, String newAlbumTitle) async { - AlbumService service = ref.watch(albumServiceProvider); - - bool isSuccess = await service.changeTitleAlbum(album, newAlbumTitle); - - if (isSuccess) { - state = state.copyWith(editTitleText: "", isEditAlbum: false); - - return true; - } - - state = state.copyWith(editTitleText: "", isEditAlbum: false); - return false; - } - - Future changeAlbumDescription(Album album, String newAlbumDescription) async { - AlbumService service = ref.watch(albumServiceProvider); - - bool isSuccess = await service.changeDescriptionAlbum(album, newAlbumDescription); - - if (isSuccess) { - state = state.copyWith(editDescriptionText: "", isEditAlbum: false); - - return true; - } - - state = state.copyWith(editDescriptionText: "", isEditAlbum: false); - - return false; - } -} - -final albumViewerProvider = StateNotifierProvider((ref) { - return AlbumViewerNotifier(ref); -}); diff --git a/mobile/lib/providers/album/current_album.provider.dart b/mobile/lib/providers/album/current_album.provider.dart deleted file mode 100644 index bd22c7a7cd..0000000000 --- a/mobile/lib/providers/album/current_album.provider.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'current_album.provider.g.dart'; - -@riverpod -class CurrentAlbum extends _$CurrentAlbum { - @override - Album? build() => null; - - void set(Album? a) => state = a; -} - -/// Mock class for testing -abstract class CurrentAlbumInternal extends _$CurrentAlbum {} diff --git a/mobile/lib/providers/album/suggested_shared_users.provider.dart b/mobile/lib/providers/album/suggested_shared_users.provider.dart deleted file mode 100644 index 51146748c7..0000000000 --- a/mobile/lib/providers/album/suggested_shared_users.provider.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; - -final otherUsersProvider = FutureProvider.autoDispose>((ref) async { - UserService userService = ref.watch(userServiceProvider); - final currentUser = ref.watch(currentUserProvider); - - final allUsers = await userService.getAll(); - allUsers.removeWhere((u) => currentUser?.id == u.id); - return allUsers; -}); diff --git a/mobile/lib/providers/api.provider.dart b/mobile/lib/providers/api.provider.dart index a54496d94c..4b3209418a 100644 --- a/mobile/lib/providers/api.provider.dart +++ b/mobile/lib/providers/api.provider.dart @@ -1,8 +1,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'api.provider.g.dart'; - -@Riverpod(keepAlive: true) -ApiService apiService(Ref _) => ApiService(); +final apiServiceProvider = Provider((_) => ApiService()); diff --git a/mobile/lib/providers/api.provider.g.dart b/mobile/lib/providers/api.provider.g.dart deleted file mode 100644 index ee1781c24c..0000000000 --- a/mobile/lib/providers/api.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'api.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$apiServiceHash() => r'187a7de59b064fab1104c23717f18ce0ae3e426c'; - -/// See also [apiService]. -@ProviderFor(apiService) -final apiServiceProvider = Provider.internal( - apiService, - name: r'apiServiceProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$apiServiceHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef ApiServiceRef = ProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 68007f283a..a5f67215a8 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -5,28 +5,17 @@ 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/extensions/platform_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.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/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/providers/notification_permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; enum AppLifeCycleEnum { active, inactive, paused, resumed, detached, hidden } @@ -87,43 +76,15 @@ class AppLifeCycleNotifier extends StateNotifier { final endpoint = await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint(); _log.info("Using server URL: $endpoint"); - if (!Store.isBetaTimelineEnabled) { - final permission = _ref.watch(galleryPermissionNotifier); - if (permission.isGranted || permission.isLimited) { - await _ref.read(backupProvider.notifier).resumeBackup(); - await _ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); - } - } - await _ref.read(serverInfoProvider.notifier).getServerVersion(); } - if (!Store.isBetaTimelineEnabled) { - switch (_ref.read(tabProvider)) { - case TabEnum.home: - await _ref.read(assetProvider.notifier).getAllAsset(); - - case TabEnum.albums: - await _ref.read(albumProvider.notifier).refreshRemoteAlbums(); - - case TabEnum.library: - case TabEnum.search: - break; - } - } else { - _ref.read(websocketProvider.notifier).connect(); - await _handleBetaTimelineResume(); - } + _ref.read(websocketProvider.notifier).connect(); + await _handleBetaTimelineResume(); await _ref.read(notificationPermissionProvider.notifier).getNotificationPermission(); await _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus(); - - if (!Store.isBetaTimelineEnabled) { - await _ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); - - _ref.invalidate(memoryFutureProvider); - } } Future _safeRun(Future action, String debugName) async { @@ -139,7 +100,6 @@ class AppLifeCycleNotifier extends StateNotifier { } Future _handleBetaTimelineResume() async { - _ref.read(backupProvider.notifier).cancelBackup(); unawaited(_ref.read(backgroundWorkerLockServiceProvider).lock()); // Give isolates time to complete any ongoing database transactions @@ -218,9 +178,7 @@ class AppLifeCycleNotifier extends StateNotifier { _pauseOperation = Completer(); try { - if (Store.isBetaTimelineEnabled) { - unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); - } + unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); await _performPause(); } catch (e, stackTrace) { _log.severe("Error during app pause", e, stackTrace); @@ -234,14 +192,7 @@ class AppLifeCycleNotifier extends StateNotifier { Future _performPause() { if (_ref.read(authProvider).isAuthenticated) { - if (!Store.isBetaTimelineEnabled) { - // Do not cancel backup if manual upload is in progress - if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) { - _ref.read(backupProvider.notifier).cancelBackup(); - } - } else { - _ref.read(driftBackupProvider.notifier).stopForegroundBackup(); - } + _ref.read(driftBackupProvider.notifier).stopForegroundBackup(); _ref.read(websocketProvider.notifier).disconnect(); } @@ -252,31 +203,12 @@ class AppLifeCycleNotifier extends StateNotifier { Future handleAppDetached() async { state = AppLifeCycleEnum.detached; - if (Store.isBetaTimelineEnabled) { - unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); - } + unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); // Flush logs before closing database try { await LogService.I.flush(); } catch (_) {} - - // Close Isar database safely - try { - final isar = Isar.getInstance(); - if (isar != null && isar.isOpen) { - await isar.close(); - } - } catch (_) {} - - if (Store.isBetaTimelineEnabled) { - return; - } - - // no guarantee this is called at all - try { - _ref.read(manualUploadProvider.notifier).cancelBackup(); - } catch (_) {} } void handleAppHidden() { diff --git a/mobile/lib/providers/app_settings.provider.dart b/mobile/lib/providers/app_settings.provider.dart index 109218a07c..3d3947a931 100644 --- a/mobile/lib/providers/app_settings.provider.dart +++ b/mobile/lib/providers/app_settings.provider.dart @@ -1,8 +1,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'app_settings.provider.g.dart'; - -@Riverpod(keepAlive: true) -AppSettingsService appSettingsService(Ref _) => const AppSettingsService(); +final appSettingsServiceProvider = Provider((_) => const AppSettingsService()); diff --git a/mobile/lib/providers/app_settings.provider.g.dart b/mobile/lib/providers/app_settings.provider.g.dart deleted file mode 100644 index c959861c04..0000000000 --- a/mobile/lib/providers/app_settings.provider.g.dart +++ /dev/null @@ -1,28 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'app_settings.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$appSettingsServiceHash() => - r'89cece3a19e06612f5639ae290120e854a0c5a31'; - -/// See also [appSettingsService]. -@ProviderFor(appSettingsService) -final appSettingsServiceProvider = Provider.internal( - appSettingsService, - name: r'appSettingsServiceProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$appSettingsServiceHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef AppSettingsServiceRef = ProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart deleted file mode 100644 index d5a4e42b74..0000000000 --- a/mobile/lib/providers/asset.provider.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -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( - ref.watch(assetServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(userServiceProvider), - ref.watch(syncServiceProvider), - ref.watch(etagServiceProvider), - ref.watch(exifServiceProvider), - ref, - ); -}); - -class AssetNotifier extends StateNotifier { - final AssetService _assetService; - final AlbumService _albumService; - final UserService _userService; - final SyncService _syncService; - final ETagService _etagService; - final ExifService _exifService; - final Ref _ref; - final log = Logger('AssetNotifier'); - bool _getAllAssetInProgress = false; - bool _deleteInProgress = false; - - AssetNotifier( - this._assetService, - this._albumService, - this._userService, - this._syncService, - this._etagService, - this._exifService, - this._ref, - ) : super(false); - - Future getAllAsset({bool clear = false}) async { - if (_getAllAssetInProgress || _deleteInProgress) { - // guard against multiple calls to this method while it's still working - return; - } - final stopwatch = Stopwatch()..start(); - try { - _getAllAssetInProgress = true; - state = true; - if (clear) { - await clearAllAssets(); - log.info("Manual refresh requested, cleared assets and albums from db"); - } - final users = await _syncService.getUsersFromServer(); - bool changedUsers = false; - if (users != null) { - changedUsers = await _syncService.syncUsersFromServer(users); - } - final bool newRemote = await _assetService.refreshRemoteAssets(); - final bool newLocal = await _albumService.refreshDeviceAlbums(); - dPrint(() => "changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal"); - if (newRemote) { - _ref.invalidate(memoryFutureProvider); - } - - log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms"); - } catch (error) { - // If there is error in getting the remote assets, still showing the new local assets - await _albumService.refreshDeviceAlbums(); - } finally { - _getAllAssetInProgress = false; - if (mounted) { - state = false; - } - } - } - - Future clearAllAssets() async { - await Store.delete(StoreKey.assetETag); - await Future.wait([ - _assetService.clearTable(), - _exifService.clearTable(), - _albumService.clearTable(), - _userService.deleteAll(), - _etagService.clearTable(), - ]); - } - - Future onNewAssetUploaded(Asset newAsset) async { - // eTag on device is not valid after partially modifying the assets - await Store.delete(StoreKey.assetETag); - await _syncService.syncNewAssetToDb(newAsset); - } - - Future deleteLocalAssets(List assets) async { - _deleteInProgress = true; - state = true; - try { - await _assetService.deleteLocalAssets(assets); - return true; - } catch (error) { - log.severe("Failed to delete local assets", error); - return false; - } finally { - _deleteInProgress = false; - state = false; - } - } - - /// Delete remote asset only - /// - /// Default behavior is trashing the asset - Future deleteRemoteAssets(Iterable deleteAssets, {bool shouldDeletePermanently = false}) async { - _deleteInProgress = true; - state = true; - try { - await _assetService.deleteRemoteAssets(deleteAssets, shouldDeletePermanently: shouldDeletePermanently); - return true; - } catch (error) { - log.severe("Failed to delete remote assets", error); - return false; - } finally { - _deleteInProgress = false; - state = false; - } - } - - Future deleteAssets(Iterable deleteAssets, {bool force = false}) async { - _deleteInProgress = true; - state = true; - try { - await _assetService.deleteAssets(deleteAssets, shouldDeletePermanently: force); - return true; - } catch (error) { - log.severe("Failed to delete assets", error); - return false; - } finally { - _deleteInProgress = false; - state = false; - } - } - - Future toggleFavorite(List assets, [bool? status]) { - status ??= !assets.every((a) => a.isFavorite); - return _assetService.changeFavoriteStatus(assets, status); - } - - Future toggleArchive(List assets, [bool? status]) { - status ??= !assets.every((a) => a.isArchived); - return _assetService.changeArchiveStatus(assets, status); - } - - Future setLockedView(List selection, AssetVisibilityEnum visibility) { - return _assetService.setVisibility(selection, visibility); - } -} - -final assetDetailProvider = StreamProvider.autoDispose.family((ref, asset) async* { - final assetService = ref.watch(assetServiceProvider); - yield await assetService.loadExif(asset); - - await for (final asset in assetService.watchAsset(asset.id)) { - if (asset != null) { - yield await ref.watch(assetServiceProvider).loadExif(asset); - } - } -}); - -final assetWatcher = StreamProvider.autoDispose.family((ref, asset) { - final assetService = ref.watch(assetServiceProvider); - return assetService.watchAsset(asset.id, fireImmediately: true); -}); diff --git a/mobile/lib/providers/asset_viewer/asset_people.provider.dart b/mobile/lib/providers/asset_viewer/asset_people.provider.dart deleted file mode 100644 index e2227920c7..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_people.provider.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'asset_people.provider.g.dart'; - -/// Maintains the list of people for an asset. -@riverpod -class AssetPeopleNotifier extends _$AssetPeopleNotifier { - final log = Logger('AssetPeopleNotifier'); - - @override - Future> build(Asset asset) async { - if (!asset.isRemote) { - return []; - } - - final list = await ref.watch(assetServiceProvider).getRemotePeopleOfAsset(asset.remoteId!); - if (list == null) { - return []; - } - - // explicitly a sorted slice to make it deterministic - // named people will be at the beginning, and names are sorted - // ascendingly - list.sort((a, b) { - final aNotEmpty = a.name.isNotEmpty; - final bNotEmpty = b.name.isNotEmpty; - if (aNotEmpty && !bNotEmpty) { - return -1; - } else if (!aNotEmpty && bNotEmpty) { - return 1; - } else if (!aNotEmpty && !bNotEmpty) { - return 0; - } - - return a.name.compareTo(b.name); - }); - return list; - } - - Future refresh() async { - // invalidate the state – this way we don't have to - // duplicate the code from build. - ref.invalidateSelf(); - } -} diff --git a/mobile/lib/providers/asset_viewer/asset_people.provider.g.dart b/mobile/lib/providers/asset_viewer/asset_people.provider.g.dart deleted file mode 100644 index 031a70e0d9..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_people.provider.g.dart +++ /dev/null @@ -1,192 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asset_people.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$assetPeopleNotifierHash() => - r'9835b180984a750c91e923e7b64dbda94f6d7574'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -abstract class _$AssetPeopleNotifier - extends - BuildlessAutoDisposeAsyncNotifier> { - late final Asset asset; - - FutureOr> build(Asset asset); -} - -/// Maintains the list of people for an asset. -/// -/// Copied from [AssetPeopleNotifier]. -@ProviderFor(AssetPeopleNotifier) -const assetPeopleNotifierProvider = AssetPeopleNotifierFamily(); - -/// Maintains the list of people for an asset. -/// -/// Copied from [AssetPeopleNotifier]. -class AssetPeopleNotifierFamily - extends Family>> { - /// Maintains the list of people for an asset. - /// - /// Copied from [AssetPeopleNotifier]. - const AssetPeopleNotifierFamily(); - - /// Maintains the list of people for an asset. - /// - /// Copied from [AssetPeopleNotifier]. - AssetPeopleNotifierProvider call(Asset asset) { - return AssetPeopleNotifierProvider(asset); - } - - @override - AssetPeopleNotifierProvider getProviderOverride( - covariant AssetPeopleNotifierProvider provider, - ) { - return call(provider.asset); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'assetPeopleNotifierProvider'; -} - -/// Maintains the list of people for an asset. -/// -/// Copied from [AssetPeopleNotifier]. -class AssetPeopleNotifierProvider - extends - AutoDisposeAsyncNotifierProviderImpl< - AssetPeopleNotifier, - List - > { - /// Maintains the list of people for an asset. - /// - /// Copied from [AssetPeopleNotifier]. - AssetPeopleNotifierProvider(Asset asset) - : this._internal( - () => AssetPeopleNotifier()..asset = asset, - from: assetPeopleNotifierProvider, - name: r'assetPeopleNotifierProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$assetPeopleNotifierHash, - dependencies: AssetPeopleNotifierFamily._dependencies, - allTransitiveDependencies: - AssetPeopleNotifierFamily._allTransitiveDependencies, - asset: asset, - ); - - AssetPeopleNotifierProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.asset, - }) : super.internal(); - - final Asset asset; - - @override - FutureOr> runNotifierBuild( - covariant AssetPeopleNotifier notifier, - ) { - return notifier.build(asset); - } - - @override - Override overrideWith(AssetPeopleNotifier Function() create) { - return ProviderOverride( - origin: this, - override: AssetPeopleNotifierProvider._internal( - () => create()..asset = asset, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - asset: asset, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement< - AssetPeopleNotifier, - List - > - createElement() { - return _AssetPeopleNotifierProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is AssetPeopleNotifierProvider && other.asset == asset; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, asset.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin AssetPeopleNotifierRef - on AutoDisposeAsyncNotifierProviderRef> { - /// The parameter `asset` of this provider. - Asset get asset; -} - -class _AssetPeopleNotifierProviderElement - extends - AutoDisposeAsyncNotifierProviderElement< - AssetPeopleNotifier, - List - > - with AssetPeopleNotifierRef { - _AssetPeopleNotifierProviderElement(super.provider); - - @override - Asset get asset => (origin as AssetPeopleNotifierProvider).asset; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart deleted file mode 100644 index 8772e3d0cb..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'asset_stack.provider.g.dart'; - -class AssetStackNotifier extends StateNotifier> { - final AssetService assetService; - final String _stackId; - - AssetStackNotifier(this.assetService, this._stackId) : super([]) { - _fetchStack(_stackId); - } - - void _fetchStack(String stackId) async { - if (!mounted) { - return; - } - - final stack = await assetService.getStackAssets(stackId); - if (stack.isNotEmpty) { - state = stack; - } - } - - void removeChild(int index) { - if (index < state.length) { - state.removeAt(index); - state = List.from(state); - } - } -} - -final assetStackStateProvider = StateNotifierProvider.autoDispose.family, String>( - (ref, stackId) => AssetStackNotifier(ref.watch(assetServiceProvider), stackId), -); - -@riverpod -int assetStackIndex(Ref _) { - return -1; -} diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart deleted file mode 100644 index dcf82cdebd..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asset_stack.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$assetStackIndexHash() => r'086ddb782e3eb38b80d755666fe35be8fe7322d7'; - -/// See also [assetStackIndex]. -@ProviderFor(assetStackIndex) -final assetStackIndexProvider = AutoDisposeProvider.internal( - assetStackIndex, - name: r'assetStackIndexProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$assetStackIndexHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef AssetStackIndexRef = AutoDisposeProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart index 19c92e7c96..96ff5f704a 100644 --- a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart +++ b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart @@ -2,7 +2,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; class AssetViewerState { final double backgroundOpacity; diff --git a/mobile/lib/providers/asset_viewer/current_asset.provider.dart b/mobile/lib/providers/asset_viewer/current_asset.provider.dart deleted file mode 100644 index 0e25660ab0..0000000000 --- a/mobile/lib/providers/asset_viewer/current_asset.provider.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'current_asset.provider.g.dart'; - -@riverpod -class CurrentAsset extends _$CurrentAsset { - @override - Asset? build() => null; - - void set(Asset? a) => state = a; -} - -/// Mock class for testing -abstract class CurrentAssetInternal extends _$CurrentAsset {} diff --git a/mobile/lib/providers/asset_viewer/current_asset.provider.g.dart b/mobile/lib/providers/asset_viewer/current_asset.provider.g.dart deleted file mode 100644 index e0d8d47d3a..0000000000 --- a/mobile/lib/providers/asset_viewer/current_asset.provider.g.dart +++ /dev/null @@ -1,26 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'current_asset.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$currentAssetHash() => r'2def10ea594152c984ae2974d687ab6856d7bdd0'; - -/// See also [CurrentAsset]. -@ProviderFor(CurrentAsset) -final currentAssetProvider = - AutoDisposeNotifierProvider.internal( - CurrentAsset.new, - name: r'currentAssetProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$currentAssetHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$CurrentAsset = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/download.provider.dart b/mobile/lib/providers/asset_viewer/download.provider.dart index a461d5766a..25db76b077 100644 --- a/mobile/lib/providers/asset_viewer/download.provider.dart +++ b/mobile/lib/providers/asset_viewer/download.provider.dart @@ -1,26 +1,15 @@ import 'dart:async'; import 'package:background_downloader/background_downloader.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/download/download_state.model.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; -import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/download.service.dart'; -import 'package:immich_mobile/services/share.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/share_dialog.dart'; class DownloadStateNotifier extends StateNotifier { final DownloadService _downloadService; - final ShareService _shareService; - final AlbumService _albumService; - DownloadStateNotifier(this._downloadService, this._shareService, this._albumService) + DownloadStateNotifier(this._downloadService) : super( const DownloadState( downloadStatus: TaskStatus.complete, @@ -132,18 +121,9 @@ class DownloadStateNotifier extends StateNotifier { if (state.taskProgress.isEmpty) { state = state.copyWith(showProgress: false); } - _albumService.refreshDeviceAlbums(); }); } - Future> downloadAllAsset(List assets) async { - return await _downloadService.downloadAll(assets); - } - - void downloadAsset(Asset asset) async { - await _downloadService.download(asset); - } - void cancelDownload(String id) async { final isCanceled = await _downloadService.cancelDownload(id); @@ -159,36 +139,8 @@ class DownloadStateNotifier extends StateNotifier { state = state.copyWith(showProgress: false); } } - - void shareAsset(Asset asset, BuildContext context) async { - unawaited( - showDialog( - context: context, - builder: (BuildContext buildContext) { - _shareService.shareAsset(asset, context).then((bool status) { - if (!status) { - ImmichToast.show( - context: context, - msg: 'image_viewer_page_state_provider_share_error'.tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - buildContext.pop(); - }); - return const ShareDialog(); - }, - barrierDismissible: false, - useRootNavigator: false, - ), - ); - } } final downloadStateProvider = StateNotifierProvider( - ((ref) => DownloadStateNotifier( - ref.watch(downloadServiceProvider), - ref.watch(shareServiceProvider), - ref.watch(albumServiceProvider), - )), + ((ref) => DownloadStateNotifier(ref.watch(downloadServiceProvider))), ); diff --git a/mobile/lib/providers/asset_viewer/render_list_status_provider.dart b/mobile/lib/providers/asset_viewer/render_list_status_provider.dart deleted file mode 100644 index 189ac85452..0000000000 --- a/mobile/lib/providers/asset_viewer/render_list_status_provider.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -enum RenderListStatusEnum { complete, empty, error, loading } - -final renderListStatusProvider = StateNotifierProvider((ref) { - return RenderListStatus(ref); -}); - -class RenderListStatus extends StateNotifier { - RenderListStatus(this.ref) : super(RenderListStatusEnum.complete); - - final Ref ref; - - RenderListStatusEnum get status => state; - - set status(RenderListStatusEnum value) { - state = value; - } -} diff --git a/mobile/lib/providers/asset_viewer/video_player_provider.dart b/mobile/lib/providers/asset_viewer/video_player_provider.dart index a4a8bd1762..8093926873 100644 --- a/mobile/lib/providers/asset_viewer/video_player_provider.dart +++ b/mobile/lib/providers/asset_viewer/video_player_provider.dart @@ -226,7 +226,7 @@ class VideoPlayerNotifier extends StateNotifier { void _startBufferingTimer() { _bufferingTimer?.cancel(); - _bufferingTimer = Timer(const Duration(seconds: 3), () { + _bufferingTimer = Timer(const Duration(seconds: 1), () { if (mounted && state.status != VideoPlaybackStatus.completed) { state = state.copyWith(status: VideoPlaybackStatus.buffering); } diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 5f3ad3d058..a6dc272313 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -1,672 +1,23 @@ import 'dart:async'; -import 'dart:io'; -import 'package:collection/collection.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'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/auth/auth_state.model.dart'; -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.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'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/backup_album.service.dart'; import 'package:immich_mobile/services/server_info.service.dart'; -import 'package:immich_mobile/utils/backup_progress.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; -import 'package:immich_mobile/utils/debug_print.dart'; -final backupProvider = StateNotifierProvider((ref) { - return BackupNotifier( - ref.watch(backupServiceProvider), - ref.watch(serverInfoServiceProvider), - ref.watch(authProvider), - ref.watch(backgroundServiceProvider), - ref.watch(galleryPermissionNotifier.notifier), - ref.watch(albumMediaRepositoryProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(backupAlbumServiceProvider), - ref, - ); +final backupProvider = StateNotifierProvider((ref) { + return BackupNotifier(ref.watch(serverInfoServiceProvider)); }); -class BackupNotifier extends StateNotifier { - BackupNotifier( - this._backupService, - this._serverInfoService, - this._authState, - this._backgroundService, - this._galleryPermissionNotifier, - this._albumMediaRepository, - this._fileMediaRepository, - this._backupAlbumService, - this.ref, - ) : super( - BackUpState( - backupProgress: BackUpProgressEnum.idle, - allAssetsInDatabase: const [], - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeeds: const [], - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - autoBackup: Store.get(StoreKey.autoBackup, false), - backgroundBackup: Store.get(StoreKey.backgroundBackup, false), - backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true), - backupRequireCharging: Store.get(StoreKey.backupRequireCharging, false), - backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay, 5000), - serverInfo: const ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0), - availableAlbums: const [], - selectedBackupAlbums: const {}, - excludedBackupAlbums: const {}, - allUniqueAssets: const {}, - selectedAlbumsBackupAssetsIds: const {}, - currentUploadAsset: CurrentUploadAsset( - id: '...', - fileCreatedAt: DateTime.parse('2020-10-04'), - fileName: '...', - fileType: '...', - fileSize: 0, - iCloudAsset: false, - ), - iCloudDownloadProgress: 0.0, - ), - ); +class BackupNotifier extends StateNotifier { + BackupNotifier(this._serverInfoService) + : super(const ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0)); - final log = Logger('BackupNotifier'); - final BackupService _backupService; final ServerInfoService _serverInfoService; - final AuthState _authState; - final BackgroundService _backgroundService; - final GalleryPermissionNotifier _galleryPermissionNotifier; - final AlbumMediaRepository _albumMediaRepository; - final FileMediaRepository _fileMediaRepository; - final BackupAlbumService _backupAlbumService; - final Ref ref; - Completer? _cancelToken; - - /// - /// UI INTERACTION - /// - /// Album selection - /// Due to the overlapping assets across multiple albums on the device - /// We have method to include and exclude albums - /// The total unique assets will be used for backing mechanism - /// - void addAlbumForBackup(AvailableAlbum album) { - if (state.excludedBackupAlbums.contains(album)) { - removeExcludedAlbumForBackup(album); - } - - state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album}); - } - - void addExcludedAlbumForBackup(AvailableAlbum album) { - if (state.selectedBackupAlbums.contains(album)) { - removeAlbumForBackup(album); - } - state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album}); - } - - void removeAlbumForBackup(AvailableAlbum album) { - Set currentSelectedAlbums = state.selectedBackupAlbums; - - currentSelectedAlbums.removeWhere((a) => a == album); - - state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums); - } - - void removeExcludedAlbumForBackup(AvailableAlbum album) { - Set currentExcludedAlbums = state.excludedBackupAlbums; - - currentExcludedAlbums.removeWhere((a) => a == album); - - state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums); - } - - Future backupAlbumSelectionDone() { - if (state.selectedBackupAlbums.isEmpty) { - // disable any backup - cancelBackup(); - setAutoBackup(false); - configureBackgroundBackup(enabled: false, onError: (msg) {}, onBatteryInfo: () {}); - } - return _updateBackupAssetCount(); - } - - void setAutoBackup(bool enabled) { - Store.put(StoreKey.autoBackup, enabled); - state = state.copyWith(autoBackup: enabled); - } - - void configureBackgroundBackup({ - bool? enabled, - bool? requireWifi, - bool? requireCharging, - int? triggerDelay, - required void Function(String msg) onError, - required void Function() onBatteryInfo, - }) async { - assert(enabled != null || requireWifi != null || requireCharging != null || triggerDelay != null); - final bool wasEnabled = state.backgroundBackup; - final bool wasWifi = state.backupRequireWifi; - final bool wasCharging = state.backupRequireCharging; - final int oldTriggerDelay = state.backupTriggerDelay; - state = state.copyWith( - backgroundBackup: enabled, - backupRequireWifi: requireWifi, - backupRequireCharging: requireCharging, - backupTriggerDelay: triggerDelay, - ); - - if (state.backgroundBackup) { - bool success = true; - if (!wasEnabled) { - if (!await _backgroundService.isIgnoringBatteryOptimizations()) { - onBatteryInfo(); - } - success &= await _backgroundService.enableService(immediate: true); - } - success &= - success && - await _backgroundService.configureService( - requireUnmetered: state.backupRequireWifi, - requireCharging: state.backupRequireCharging, - triggerUpdateDelay: state.backupTriggerDelay, - triggerMaxDelay: state.backupTriggerDelay * 10, - ); - if (success) { - await Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi); - await Store.put(StoreKey.backupRequireCharging, state.backupRequireCharging); - await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay); - await Store.put(StoreKey.backgroundBackup, state.backgroundBackup); - } else { - state = state.copyWith( - backgroundBackup: wasEnabled, - backupRequireWifi: wasWifi, - backupRequireCharging: wasCharging, - backupTriggerDelay: oldTriggerDelay, - ); - onError("backup_controller_page_background_configure_error"); - } - } else { - final bool success = await _backgroundService.disableService(); - if (!success) { - state = state.copyWith(backgroundBackup: wasEnabled); - onError("backup_controller_page_background_configure_error"); - } - } - } - - /// - /// Get all album on the device - /// Get all selected and excluded album from the user's persistent storage - /// If this is the first time performing backup - set the default selected album to be - /// the one that has all assets (`Recent` on Android, `Recents` on iOS) - /// - Future _getBackupAlbumsInfo() async { - Stopwatch stopwatch = Stopwatch()..start(); - // Get all albums on the device - List availableAlbums = []; - List albums = await _albumMediaRepository.getAll(); - - // Map of id -> album for quick album lookup later on. - Map albumMap = {}; - - log.info('Found ${albums.length} local albums'); - - for (Album album in albums) { - AvailableAlbum availableAlbum = AvailableAlbum( - album: album, - assetCount: await ref.read(albumMediaRepositoryProvider).getAssetCount(album.localId!), - ); - - availableAlbums.add(availableAlbum); - - albumMap[album.localId!] = album; - } - state = state.copyWith(availableAlbums: availableAlbums); - - final List excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - final List selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select); - - final Set selectedAlbums = {}; - for (final BackupAlbum ba in selectedBackupAlbums) { - final albumAsset = albumMap[ba.id]; - - if (albumAsset != null) { - selectedAlbums.add( - AvailableAlbum( - album: albumAsset, - assetCount: await _albumMediaRepository.getAssetCount(albumAsset.localId!), - lastBackup: ba.lastBackup, - ), - ); - } else { - log.severe('Selected album not found'); - } - } - - final Set excludedAlbums = {}; - for (final BackupAlbum ba in excludedBackupAlbums) { - final albumAsset = albumMap[ba.id]; - - if (albumAsset != null) { - excludedAlbums.add( - AvailableAlbum( - album: albumAsset, - assetCount: await ref.read(albumMediaRepositoryProvider).getAssetCount(albumAsset.localId!), - lastBackup: ba.lastBackup, - ), - ); - } else { - log.severe('Excluded album not found'); - } - } - - state = state.copyWith(selectedBackupAlbums: selectedAlbums, excludedBackupAlbums: excludedAlbums); - - log.info("_getBackupAlbumsInfo: Found ${availableAlbums.length} available albums"); - dPrint(() => "_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms"); - } - - /// - /// From all the selected and albums assets - /// Find the assets that are not overlapping between the two sets - /// Those assets are unique and are used as the total assets - /// - Future _updateBackupAssetCount() async { - // Save to persistent storage - await _updatePersistentAlbumsSelection(); - - final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds(); - final Set assetsFromSelectedAlbums = {}; - final Set assetsFromExcludedAlbums = {}; - - for (final album in state.selectedBackupAlbums) { - final assetCount = await ref.read(albumMediaRepositoryProvider).getAssetCount(album.album.localId!); - - if (assetCount == 0) { - continue; - } - - final assets = await ref.read(albumMediaRepositoryProvider).getAssets(album.album.localId!); - - // Add album's name to the asset info - for (final asset in assets) { - List albumNames = [album.name]; - - final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull((a) => a.asset.localId == asset.localId); - - if (existingAsset != null) { - albumNames.addAll(existingAsset.albumNames); - assetsFromSelectedAlbums.remove(existingAsset); - } - - assetsFromSelectedAlbums.add(BackupCandidate(asset: asset, albumNames: albumNames)); - } - } - - for (final album in state.excludedBackupAlbums) { - final assetCount = await ref.read(albumMediaRepositoryProvider).getAssetCount(album.album.localId!); - - if (assetCount == 0) { - continue; - } - - final assets = await ref.read(albumMediaRepositoryProvider).getAssets(album.album.localId!); - - for (final asset in assets) { - assetsFromExcludedAlbums.add(BackupCandidate(asset: asset, albumNames: [album.name])); - } - } - - final Set allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); - - final allAssetsInDatabase = await _backupService.getDeviceBackupAsset(); - - if (allAssetsInDatabase == null) { - return; - } - - // Find asset that were backup from selected albums - final Set selectedAlbumsBackupAssets = Set.from(allUniqueAssets.map((e) => e.asset.localId)); - - selectedAlbumsBackupAssets.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId)); - - // Remove duplicated asset from all unique assets - allUniqueAssets.removeWhere((candidate) => duplicatedAssetIds.contains(candidate.asset.localId)); - - if (allUniqueAssets.isEmpty) { - log.info("No assets are selected for back up"); - state = state.copyWith( - backupProgress: BackUpProgressEnum.idle, - allAssetsInDatabase: allAssetsInDatabase, - allUniqueAssets: {}, - selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets, - ); - } else { - state = state.copyWith( - allAssetsInDatabase: allAssetsInDatabase, - allUniqueAssets: allUniqueAssets, - selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets, - ); - } - } - - /// Get all necessary information for calculating the available albums, - /// which albums are selected or excluded - /// and then update the UI according to those information - Future getBackupInfo() async { - final isEnabled = await _backgroundService.isBackgroundBackupEnabled(); - - state = state.copyWith(backgroundBackup: isEnabled); - if (isEnabled != Store.get(StoreKey.backgroundBackup, !isEnabled)) { - await Store.put(StoreKey.backgroundBackup, isEnabled); - } - - if (state.backupProgress != BackUpProgressEnum.inBackground) { - await _getBackupAlbumsInfo(); - await updateDiskInfo(); - await _updateBackupAssetCount(); - } else { - log.warning("cannot get backup info - background backup is in progress!"); - } - } - - /// Save user selection of selected albums and excluded albums to database - Future _updatePersistentAlbumsSelection() async { - final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); - final selected = state.selectedBackupAlbums.map( - (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.select), - ); - final excluded = state.excludedBackupAlbums.map( - (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.exclude), - ); - final candidates = selected.followedBy(excluded).toList(); - candidates.sortBy((e) => e.id); - - final savedBackupAlbums = await _backupAlbumService.getAll(sort: BackupAlbumSort.id); - final List toDelete = []; - final List toUpsert = []; - - diffSortedListsSync( - savedBackupAlbums, - candidates, - compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id), - both: (BackupAlbum a, BackupAlbum b) { - b.lastBackup = a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup; - toUpsert.add(b); - return true; - }, - onlyFirst: (BackupAlbum a) => toDelete.add(a.isarId), - onlySecond: (BackupAlbum b) => toUpsert.add(b), - ); - - await _backupAlbumService.deleteAll(toDelete); - await _backupAlbumService.updateAll(toUpsert); - } - - /// Invoke backup process - Future startBackupProcess() async { - dPrint(() => "Start backup process"); - assert(state.backupProgress == BackUpProgressEnum.idle); - state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress); - - await getBackupInfo(); - - final hasPermission = _galleryPermissionNotifier.hasPermission; - if (hasPermission) { - await _fileMediaRepository.clearFileCache(); - - if (state.allUniqueAssets.isEmpty) { - log.info("No Asset On Device - Abort Backup Process"); - state = state.copyWith(backupProgress: BackUpProgressEnum.idle); - return; - } - - Set assetsWillBeBackup = Set.from(state.allUniqueAssets); - // Remove item that has already been backed up - for (final assetId in state.allAssetsInDatabase) { - assetsWillBeBackup.removeWhere((e) => e.asset.localId == assetId); - } - - if (assetsWillBeBackup.isEmpty) { - state = state.copyWith(backupProgress: BackUpProgressEnum.idle); - } - - // Perform Backup - _cancelToken?.complete(); - _cancelToken = Completer(); - - final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - - pmProgressHandler?.stream.listen((event) { - final double progress = event.progress; - state = state.copyWith(iCloudDownloadProgress: progress); - }); - - await _backupService.backupAsset( - assetsWillBeBackup, - _cancelToken!, - pmProgressHandler: pmProgressHandler, - onSuccess: _onAssetUploaded, - onProgress: _onUploadProgress, - onCurrentAsset: _onSetCurrentBackupAsset, - onError: _onBackupError, - ); - await notifyBackgroundServiceCanRun(); - } else { - await openAppSettings(); - } - } - - void setAvailableAlbums(availableAlbums) { - state = state.copyWith(availableAlbums: availableAlbums); - } - - void _onBackupError(ErrorUploadAsset errorAssetInfo) { - ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo); - } - - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) { - state = state.copyWith(currentUploadAsset: currentUploadAsset); - } - - void cancelBackup() { - if (state.backupProgress != BackUpProgressEnum.inProgress) { - notifyBackgroundServiceCanRun(); - } - _cancelToken?.complete(); - _cancelToken = null; - state = state.copyWith( - backupProgress: BackUpProgressEnum.idle, - progressInPercentage: 0.0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - ); - } - - void _onAssetUploaded(SuccessUploadAsset result) async { - if (result.isDuplicate) { - state = state.copyWith( - allUniqueAssets: state.allUniqueAssets - .where((candidate) => candidate.asset.localId != result.candidate.asset.localId) - .toSet(), - ); - } else { - state = state.copyWith( - selectedAlbumsBackupAssetsIds: {...state.selectedAlbumsBackupAssetsIds, result.candidate.asset.localId!}, - allAssetsInDatabase: [...state.allAssetsInDatabase, result.candidate.asset.localId!], - ); - } - - if (state.allUniqueAssets.length - state.selectedAlbumsBackupAssetsIds.length == 0) { - final latestAssetBackup = state.allUniqueAssets - .map((candidate) => candidate.asset.fileModifiedAt) - .reduce((v, e) => e.isAfter(v) ? e : v); - state = state.copyWith( - selectedBackupAlbums: state.selectedBackupAlbums.map((e) => e.copyWith(lastBackup: latestAssetBackup)).toSet(), - excludedBackupAlbums: state.excludedBackupAlbums.map((e) => e.copyWith(lastBackup: latestAssetBackup)).toSet(), - backupProgress: BackUpProgressEnum.done, - progressInPercentage: 0.0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - ); - await _updatePersistentAlbumsSelection(); - } - - await updateDiskInfo(); - } - - void _onUploadProgress(int sent, int total) { - double lastUploadSpeed = state.progressInFileSpeed; - List lastUploadSpeeds = state.progressInFileSpeeds.toList(); - DateTime lastUpdateTime = state.progressInFileSpeedUpdateTime; - int lastSentBytes = state.progressInFileSpeedUpdateSentBytes; - - final now = DateTime.now(); - final duration = now.difference(lastUpdateTime); - - // Keep the upload speed average span limited, to keep it somewhat relevant - if (lastUploadSpeeds.length > 10) { - lastUploadSpeeds.removeAt(0); - } - - if (duration.inSeconds > 0) { - lastUploadSpeeds.add(((sent - lastSentBytes) / duration.inSeconds).abs().roundToDouble()); - - lastUploadSpeed = lastUploadSpeeds.average.abs().roundToDouble(); - lastUpdateTime = now; - lastSentBytes = sent; - } - - state = state.copyWith( - progressInPercentage: (sent.toDouble() / total.toDouble() * 100), - progressInFileSize: humanReadableFileBytesProgress(sent, total), - progressInFileSpeed: lastUploadSpeed, - progressInFileSpeeds: lastUploadSpeeds, - progressInFileSpeedUpdateTime: lastUpdateTime, - progressInFileSpeedUpdateSentBytes: lastSentBytes, - ); - } Future updateDiskInfo() async { final diskInfo = await _serverInfoService.getDiskInfo(); - - // Update server info if (diskInfo != null) { - state = state.copyWith(serverInfo: diskInfo); + state = diskInfo; } } - - Future _resumeBackup() async { - // Check if user is login - final accessKey = Store.tryGet(StoreKey.accessToken); - - // User has been logged out return - if (accessKey == null || !_authState.isAuthenticated) { - log.info("[_resumeBackup] not authenticated - abort"); - return; - } - - // Check if this device is enable backup by the user - if (state.autoBackup) { - // check if backup is already in process - then return - if (state.backupProgress == BackUpProgressEnum.inProgress) { - log.info("[_resumeBackup] Auto Backup is already in progress - abort"); - return; - } - - if (state.backupProgress == BackUpProgressEnum.inBackground) { - log.info("[_resumeBackup] Background backup is running - abort"); - return; - } - - if (state.backupProgress == BackUpProgressEnum.manualInProgress) { - log.info("[_resumeBackup] Manual upload is running - abort"); - return; - } - - // Run backup - log.info("[_resumeBackup] Start back up"); - await startBackupProcess(); - } - return; - } - - Future resumeBackup() async { - final List selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select); - final List excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - Set selectedAlbums = state.selectedBackupAlbums; - Set excludedAlbums = state.excludedBackupAlbums; - if (selectedAlbums.isNotEmpty) { - selectedAlbums = _updateAlbumsBackupTime(selectedAlbums, selectedBackupAlbums); - } - - if (excludedAlbums.isNotEmpty) { - excludedAlbums = _updateAlbumsBackupTime(excludedAlbums, excludedBackupAlbums); - } - final BackUpProgressEnum previous = state.backupProgress; - state = state.copyWith( - backupProgress: BackUpProgressEnum.inBackground, - selectedBackupAlbums: selectedAlbums, - excludedBackupAlbums: excludedAlbums, - ); - // assumes the background service is currently running - // if true, waits until it has stopped to start the backup - final bool hasLock = await _backgroundService.acquireLock(); - if (hasLock) { - state = state.copyWith(backupProgress: previous); - } - return _resumeBackup(); - } - - Set _updateAlbumsBackupTime(Set albums, List backupAlbums) { - Set result = {}; - for (BackupAlbum ba in backupAlbums) { - try { - AvailableAlbum a = albums.firstWhere((e) => e.id == ba.id); - result.add(a.copyWith(lastBackup: ba.lastBackup)); - } on StateError { - log.severe("[_updateAlbumBackupTime] failed to find album in state", "State Error", StackTrace.current); - } - } - return result; - } - - Future notifyBackgroundServiceCanRun() async { - const allowedStates = [AppLifeCycleEnum.inactive, AppLifeCycleEnum.paused, AppLifeCycleEnum.detached]; - if (allowedStates.contains(ref.read(appStateProvider.notifier).state)) { - _backgroundService.releaseLock(); - } - } - - BackUpProgressEnum get backupProgress => state.backupProgress; - - void updateBackupProgress(BackUpProgressEnum backupProgress) { - state = state.copyWith(backupProgress: backupProgress); - } } diff --git a/mobile/lib/providers/backup/backup_verification.provider.dart b/mobile/lib/providers/backup/backup_verification.provider.dart deleted file mode 100644 index 50270e87ca..0000000000 --- a/mobile/lib/providers/backup/backup_verification.provider.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'dart:async'; - -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/services/backup_verification.service.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; - -part 'backup_verification.provider.g.dart'; - -@riverpod -class BackupVerification extends _$BackupVerification { - @override - bool build() => false; - - void performBackupCheck(BuildContext context) async { - try { - state = true; - final backupState = ref.read(backupProvider); - - if (backupState.allUniqueAssets.length > backupState.selectedAlbumsBackupAssetsIds.length) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "Backup all assets before starting this check!", - toastType: ToastType.error, - ); - } - return; - } - final connection = await Connectivity().checkConnectivity(); - if (!connection.contains(ConnectivityResult.wifi)) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "Make sure to be connected to unmetered Wi-Fi", - toastType: ToastType.error, - ); - } - return; - } - unawaited(WakelockPlus.enable()); - - const limit = 100; - final toDelete = await ref.read(backupVerificationServiceProvider).findWronglyBackedUpAssets(limit: limit); - if (toDelete.isEmpty) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "Did not find any corrupt asset backups!", - toastType: ToastType.success, - ); - } - } else { - if (context.mounted) { - await showDialog( - context: context, - builder: (ctx) => ConfirmDialog( - onOk: () => _performDeletion(context, toDelete), - title: "Corrupt backups!", - ok: "Delete", - content: - "Found ${toDelete.length} (max $limit at once) corrupt asset backups. " - "Run the check again to find more.\n" - "Do you want to delete the corrupt asset backups now?", - ), - ); - } - } - } finally { - unawaited(WakelockPlus.disable()); - state = false; - } - } - - Future _performDeletion(BuildContext context, List assets) async { - try { - state = true; - if (context.mounted) { - ImmichToast.show(context: context, msg: "Deleting ${assets.length} assets on the server..."); - } - await ref.read(assetProvider.notifier).deleteAssets(assets, force: true); - if (context.mounted) { - ImmichToast.show( - context: context, - msg: - "Deleted ${assets.length} assets on the server. " - "You can now start a manual backup", - toastType: ToastType.success, - ); - } - } finally { - state = false; - } - } -} diff --git a/mobile/lib/providers/backup/backup_verification.provider.g.dart b/mobile/lib/providers/backup/backup_verification.provider.g.dart deleted file mode 100644 index 13f6819fa7..0000000000 --- a/mobile/lib/providers/backup/backup_verification.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'backup_verification.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$backupVerificationHash() => - r'b4b34909ed1af3f28877ea457d53a4a18b6417f8'; - -/// See also [BackupVerification]. -@ProviderFor(BackupVerification) -final backupVerificationProvider = - AutoDisposeNotifierProvider.internal( - BackupVerification.new, - name: r'backupVerificationProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$backupVerificationHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$BackupVerification = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/backup/error_backup_list.provider.dart b/mobile/lib/providers/backup/error_backup_list.provider.dart deleted file mode 100644 index db116e4bb9..0000000000 --- a/mobile/lib/providers/backup/error_backup_list.provider.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; - -class ErrorBackupListNotifier extends StateNotifier> { - ErrorBackupListNotifier() : super({}); - - add(ErrorUploadAsset errorAsset) { - state = state.union({errorAsset}); - } - - remove(ErrorUploadAsset errorAsset) { - state = state.difference({errorAsset}); - } - - empty() { - state = {}; - } -} - -final errorBackupListProvider = StateNotifierProvider>( - (ref) => ErrorBackupListNotifier(), -); diff --git a/mobile/lib/providers/backup/ios_background_settings.provider.dart b/mobile/lib/providers/backup/ios_background_settings.provider.dart deleted file mode 100644 index 98d55882cc..0000000000 --- a/mobile/lib/providers/backup/ios_background_settings.provider.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/services/background.service.dart'; - -class IOSBackgroundSettings { - final bool appRefreshEnabled; - final int numberOfBackgroundTasksQueued; - final DateTime? timeOfLastFetch; - final DateTime? timeOfLastProcessing; - - const IOSBackgroundSettings({ - required this.appRefreshEnabled, - required this.numberOfBackgroundTasksQueued, - this.timeOfLastFetch, - this.timeOfLastProcessing, - }); -} - -class IOSBackgroundSettingsNotifier extends StateNotifier { - final BackgroundService _service; - IOSBackgroundSettingsNotifier(this._service) : super(null); - - IOSBackgroundSettings? get settings => state; - - Future refresh() async { - final lastFetchTime = await _service.getIOSBackupLastRun(IosBackgroundTask.fetch); - final lastProcessingTime = await _service.getIOSBackupLastRun(IosBackgroundTask.processing); - int numberOfProcesses = await _service.getIOSBackupNumberOfProcesses(); - final appRefreshEnabled = await _service.getIOSBackgroundAppRefreshEnabled(); - - // If this is enabled and there are no background processes, - // the user just enabled app refresh in Settings. - // But we don't have any background services running, since it was disabled - // before. - if (await _service.isBackgroundBackupEnabled() && numberOfProcesses == 0) { - // We need to restart the background service - await _service.enableService(); - numberOfProcesses = await _service.getIOSBackupNumberOfProcesses(); - } - - final settings = IOSBackgroundSettings( - appRefreshEnabled: appRefreshEnabled, - numberOfBackgroundTasksQueued: numberOfProcesses, - timeOfLastFetch: lastFetchTime, - timeOfLastProcessing: lastProcessingTime, - ); - - state = settings; - return settings; - } -} - -final iOSBackgroundSettingsProvider = StateNotifierProvider( - (ref) => IOSBackgroundSettingsNotifier(ref.watch(backgroundServiceProvider)), -); diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart deleted file mode 100644 index 40efcd7422..0000000000 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ /dev/null @@ -1,391 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/widgets.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/manual_upload_state.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.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/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.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/background.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/backup_album.service.dart'; -import 'package:immich_mobile/services/local_notification.service.dart'; -import 'package:immich_mobile/utils/backup_progress.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -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; - -final manualUploadProvider = StateNotifierProvider((ref) { - return ManualUploadNotifier( - ref.watch(localNotificationService), - ref.watch(backupProvider.notifier), - ref.watch(backupServiceProvider), - ref.watch(backupAlbumServiceProvider), - ref, - ); -}); - -class ManualUploadNotifier extends StateNotifier { - final Logger _log = Logger("ManualUploadNotifier"); - final LocalNotificationService _localNotificationService; - final BackupNotifier _backupProvider; - final BackupService _backupService; - final BackupAlbumService _backupAlbumService; - final Ref ref; - Completer? _cancelToken; - - ManualUploadNotifier( - this._localNotificationService, - this._backupProvider, - this._backupService, - this._backupAlbumService, - this.ref, - ) : super( - ManualUploadState( - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeeds: const [], - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - currentUploadAsset: CurrentUploadAsset( - id: '...', - fileCreatedAt: DateTime.parse('2020-10-04'), - fileName: '...', - fileType: '...', - ), - totalAssetsToUpload: 0, - successfulUploads: 0, - currentAssetIndex: 0, - showDetailedNotification: false, - ), - ); - - String _lastPrintedDetailContent = ''; - String? _lastPrintedDetailTitle; - - static const notifyInterval = Duration(milliseconds: 500); - late final ThrottleProgressUpdate _throttledNotifiy = ThrottleProgressUpdate(_updateProgress, notifyInterval); - late final ThrottleProgressUpdate _throttledDetailNotify = ThrottleProgressUpdate( - _updateDetailProgress, - notifyInterval, - ); - - void _updateProgress(String? title, int progress, int total) { - // Guard against throttling calling this method after the upload is done - if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - _localNotificationService.showOrUpdateManualUploadStatus( - "backup_background_service_in_progress_notification".tr(), - formatAssetBackupProgress(state.currentAssetIndex, state.totalAssetsToUpload), - maxProgress: state.totalAssetsToUpload, - progress: state.currentAssetIndex, - showActions: true, - ); - } - } - - void _updateDetailProgress(String? title, int progress, int total) { - // Guard against throttling calling this method after the upload is done - if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - final String msg = total > 0 ? humanReadableBytesProgress(progress, total) : ""; - // only update if message actually differs (to stop many useless notification updates on large assets or slow connections) - if (msg != _lastPrintedDetailContent || title != _lastPrintedDetailTitle) { - _lastPrintedDetailContent = msg; - _lastPrintedDetailTitle = title; - _localNotificationService.showOrUpdateManualUploadStatus( - title ?? 'Uploading', - msg, - progress: total > 0 ? (progress * 1000) ~/ total : 0, - maxProgress: 1000, - isDetailed: true, - // Detailed noitifcation is displayed for Single asset uploads. Show actions for such case - showActions: state.totalAssetsToUpload == 1, - ); - } - } - } - - void _onAssetUploaded(SuccessUploadAsset result) { - state = state.copyWith(successfulUploads: state.successfulUploads + 1); - _backupProvider.updateDiskInfo(); - } - - void _onAssetUploadError(ErrorUploadAsset errorAssetInfo) { - ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo); - } - - void _onProgress(int sent, int total) { - double lastUploadSpeed = state.progressInFileSpeed; - List lastUploadSpeeds = state.progressInFileSpeeds.toList(); - DateTime lastUpdateTime = state.progressInFileSpeedUpdateTime; - int lastSentBytes = state.progressInFileSpeedUpdateSentBytes; - - final now = DateTime.now(); - final duration = now.difference(lastUpdateTime); - - // Keep the upload speed average span limited, to keep it somewhat relevant - if (lastUploadSpeeds.length > 10) { - lastUploadSpeeds.removeAt(0); - } - - if (duration.inSeconds > 0) { - lastUploadSpeeds.add(((sent - lastSentBytes) / duration.inSeconds).abs().roundToDouble()); - - lastUploadSpeed = lastUploadSpeeds.average.abs().roundToDouble(); - lastUpdateTime = now; - lastSentBytes = sent; - } - - state = state.copyWith( - progressInPercentage: (sent.toDouble() / total.toDouble() * 100), - progressInFileSize: humanReadableFileBytesProgress(sent, total), - progressInFileSpeed: lastUploadSpeed, - progressInFileSpeeds: lastUploadSpeeds, - progressInFileSpeedUpdateTime: lastUpdateTime, - progressInFileSpeedUpdateSentBytes: lastSentBytes, - ); - - if (state.showDetailedNotification) { - final title = "backup_background_service_current_upload_notification".tr( - namedArgs: {'filename': state.currentUploadAsset.fileName}, - ); - _throttledDetailNotify(title: title, progress: sent, total: total); - } - } - - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) { - state = state.copyWith(currentUploadAsset: currentUploadAsset, currentAssetIndex: state.currentAssetIndex + 1); - if (state.totalAssetsToUpload > 1) { - _throttledNotifiy(); - } - if (state.showDetailedNotification) { - _throttledDetailNotify.title = "backup_background_service_current_upload_notification".tr( - namedArgs: {'filename': currentUploadAsset.fileName}, - ); - _throttledDetailNotify.progress = 0; - _throttledDetailNotify.total = 0; - } - } - - Future _startUpload(Iterable allManualUploads) async { - bool hasErrors = false; - try { - _backupProvider.updateBackupProgress(BackUpProgressEnum.manualInProgress); - - if (ref.read(galleryPermissionNotifier.notifier).hasPermission) { - await ref.read(fileMediaRepositoryProvider).clearFileCache(); - - final allAssetsFromDevice = allManualUploads.where((e) => e.isLocal && !e.isRemote).toList(); - - if (allAssetsFromDevice.length != allManualUploads.length) { - _log.warning( - '[_startUpload] Refreshed upload list -> ${allManualUploads.length - allAssetsFromDevice.length} asset will not be uploaded', - ); - } - - final selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select); - final excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - - // Get candidates from selected albums and excluded albums - Set candidates = await _backupService.buildUploadCandidates( - selectedBackupAlbums, - excludedBackupAlbums, - useTimeFilter: false, - ); - - // Extrack candidate from allAssetsFromDevice - final uploadAssets = candidates.where( - (candidate) => - allAssetsFromDevice.firstWhereOrNull((asset) => asset.localId == candidate.asset.localId) != null, - ); - - if (uploadAssets.isEmpty) { - dPrint(() => "[_startUpload] No Assets to upload - Abort Process"); - _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); - return false; - } - - state = state.copyWith( - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - totalAssetsToUpload: uploadAssets.length, - successfulUploads: 0, - currentAssetIndex: 0, - currentUploadAsset: CurrentUploadAsset( - id: '...', - fileCreatedAt: DateTime.parse('2020-10-04'), - fileName: '...', - fileType: '...', - ), - ); - // Reset Error List - ref.watch(errorBackupListProvider.notifier).empty(); - - if (state.totalAssetsToUpload > 1) { - _throttledNotifiy(); - } - - // Show detailed asset if enabled in settings or if a single asset is uploaded - bool showDetailedNotification = - ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.backgroundBackupSingleProgress) || - state.totalAssetsToUpload == 1; - state = state.copyWith(showDetailedNotification: showDetailedNotification); - final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - - _cancelToken?.complete(); - _cancelToken = Completer(); - final bool ok = await ref - .read(backupServiceProvider) - .backupAsset( - uploadAssets, - _cancelToken!, - pmProgressHandler: pmProgressHandler, - onSuccess: _onAssetUploaded, - onProgress: _onProgress, - onCurrentAsset: _onSetCurrentBackupAsset, - onError: _onAssetUploadError, - ); - - // Close detailed notification - await _localNotificationService.closeNotification(LocalNotificationService.manualUploadDetailedNotificationID); - - _log.info( - '[_startUpload] Manual Upload Completed - success: ${state.successfulUploads},' - ' failed: ${state.totalAssetsToUpload - state.successfulUploads}', - ); - - // User cancelled upload - if (!ok && _cancelToken == null) { - await _localNotificationService.showOrUpdateManualUploadStatus( - "backup_manual_title".tr(), - "backup_manual_cancelled".tr(), - presentBanner: true, - ); - hasErrors = true; - } else if (state.successfulUploads == 0 || (!ok && _cancelToken != null)) { - await _localNotificationService.showOrUpdateManualUploadStatus( - "backup_manual_title".tr(), - "failed".tr(), - presentBanner: true, - ); - hasErrors = true; - } else { - await _localNotificationService.showOrUpdateManualUploadStatus( - "backup_manual_title".tr(), - "backup_manual_success".tr(), - presentBanner: true, - ); - } - } else { - unawaited(openAppSettings()); - dPrint(() => "[_startUpload] Do not have permission to the gallery"); - } - } catch (e) { - dPrint(() => "ERROR _startUpload: ${e.toString()}"); - hasErrors = true; - } finally { - _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); - _handleAppInActivity(); - await _localNotificationService.closeNotification(LocalNotificationService.manualUploadDetailedNotificationID); - await _backupProvider.notifyBackgroundServiceCanRun(); - } - return !hasErrors; - } - - void _handleAppInActivity() { - final appState = ref.read(appStateProvider.notifier).getAppState(); - // The app is currently in background. Perform the necessary cleanups which - // are on-hold for upload completion - if (appState != AppLifeCycleEnum.active && appState != AppLifeCycleEnum.resumed) { - ref.read(backupProvider.notifier).cancelBackup(); - } - } - - void cancelBackup() { - if (_backupProvider.backupProgress != BackUpProgressEnum.inProgress && - _backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) { - _backupProvider.notifyBackgroundServiceCanRun(); - } - _cancelToken?.complete(); - _cancelToken = null; - if (_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) { - _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); - } - state = state.copyWith( - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - ); - } - - Future uploadAssets(BuildContext context, Iterable allManualUploads) async { - // assumes the background service is currently running and - // waits until it has stopped to start the backup. - final bool hasLock = await ref.read(backgroundServiceProvider).acquireLock(); - if (!hasLock) { - dPrint(() => "[uploadAssets] could not acquire lock, exiting"); - ImmichToast.show( - context: context, - msg: "failed".tr(), - toastType: ToastType.info, - gravity: ToastGravity.BOTTOM, - durationInSecond: 3, - ); - return false; - } - - bool showInProgress = false; - - // check if backup is already in process - then return - if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - dPrint(() => "[uploadAssets] Manual upload is already running - abort"); - showInProgress = true; - } - - if (_backupProvider.backupProgress == BackUpProgressEnum.inProgress) { - dPrint(() => "[uploadAssets] Auto Backup is already in progress - abort"); - showInProgress = true; - return false; - } - - if (_backupProvider.backupProgress == BackUpProgressEnum.inBackground) { - dPrint(() => "[uploadAssets] Background backup is running - abort"); - showInProgress = true; - } - - if (showInProgress) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "backup_manual_in_progress".tr(), - toastType: ToastType.info, - gravity: ToastGravity.BOTTOM, - durationInSecond: 3, - ); - } - return false; - } - - return _startUpload(allManualUploads); - } -} diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index fea95f42aa..b298514d67 100644 --- a/mobile/lib/providers/cast.provider.dart +++ b/mobile/lib/providers/cast.provider.dart @@ -1,6 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as old_asset_entity; import 'package:immich_mobile/models/cast/cast_manager_state.dart'; import 'package:immich_mobile/services/gcast.service.dart'; @@ -55,26 +54,6 @@ class CastNotifier extends StateNotifier { _gCastService.loadMedia(asset, reload); } - // TODO: remove this when we migrate to new timeline - void loadMediaOld(old_asset_entity.Asset asset, bool reload) { - final remoteAsset = RemoteAsset( - id: asset.remoteId.toString(), - name: asset.name, - ownerId: asset.ownerId.toString(), - checksum: asset.checksum, - type: asset.type == old_asset_entity.AssetType.image - ? AssetType.image - : asset.type == old_asset_entity.AssetType.video - ? AssetType.video - : AssetType.other, - createdAt: asset.fileCreatedAt, - updatedAt: asset.updatedAt, - isEdited: false, - ); - - _gCastService.loadMedia(remoteAsset, reload); - } - Future connect(CastDestinationType type, dynamic device) async { switch (type) { case CastDestinationType.googleCast: diff --git a/mobile/lib/providers/db.provider.dart b/mobile/lib/providers/db.provider.dart deleted file mode 100644 index e03e037f36..0000000000 --- a/mobile/lib/providers/db.provider.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:isar/isar.dart'; - -// overwritten in main.dart due to async loading -final dbProvider = Provider((_) => throw UnimplementedError()); diff --git a/mobile/lib/providers/folder.provider.dart b/mobile/lib/providers/folder.provider.dart index 696d7e19fd..816a88996e 100644 --- a/mobile/lib/providers/folder.provider.dart +++ b/mobile/lib/providers/folder.provider.dart @@ -1,8 +1,8 @@ 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/models/folder/root_folder.model.dart'; import 'package:immich_mobile/services/folder.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:logging/logging.dart'; class FolderStructureNotifier extends StateNotifier> { @@ -26,7 +26,7 @@ final folderStructureProvider = StateNotifierProvider> { +class FolderRenderListNotifier extends StateNotifier>> { final FolderService _folderService; final RootFolder _folder; final Logger _log = Logger("FolderAssetsNotifier"); @@ -36,8 +36,7 @@ class FolderRenderListNotifier extends StateNotifier> { Future fetchAssets(SortOrder order) async { try { final assets = await _folderService.getFolderAssets(_folder, order); - final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.none); - state = AsyncData(renderList); + state = AsyncData(assets); } catch (e, stack) { _log.severe("Failed to fetch folder assets", e, stack); state = AsyncError(e, stack); @@ -46,6 +45,9 @@ class FolderRenderListNotifier extends StateNotifier> { } final folderRenderListProvider = - StateNotifierProvider.family, RootFolder>((ref, folder) { + StateNotifierProvider.family>, RootFolder>(( + ref, + folder, + ) { return FolderRenderListNotifier(ref.watch(folderServiceProvider), folder); }); diff --git a/mobile/lib/providers/image/exceptions/image_loading_exception.dart b/mobile/lib/providers/image/exceptions/image_loading_exception.dart deleted file mode 100644 index 98f633a88f..0000000000 --- a/mobile/lib/providers/image/exceptions/image_loading_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -/// An exception for the [ImageLoader] and the Immich image providers -class ImageLoadingException implements Exception { - final String message; - const ImageLoadingException(this.message); -} diff --git a/mobile/lib/providers/immich_logo_provider.dart b/mobile/lib/providers/immich_logo_provider.dart index b24294fc2e..d9e51eccac 100644 --- a/mobile/lib/providers/immich_logo_provider.dart +++ b/mobile/lib/providers/immich_logo_provider.dart @@ -2,13 +2,9 @@ import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'immich_logo_provider.g.dart'; - -@riverpod -Future immichLogo(Ref _) async { +final immichLogoProvider = FutureProvider.autoDispose((ref) async { final json = await rootBundle.loadString('assets/immich-logo.json'); final j = jsonDecode(json); return base64Decode(j['content']); -} +}); diff --git a/mobile/lib/providers/immich_logo_provider.g.dart b/mobile/lib/providers/immich_logo_provider.g.dart deleted file mode 100644 index f1af433c1b..0000000000 --- a/mobile/lib/providers/immich_logo_provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'immich_logo_provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$immichLogoHash() => r'6de7fcca1ef9acef6ab7398eb0c664080747e0ea'; - -/// See also [immichLogo]. -@ProviderFor(immichLogo) -final immichLogoProvider = AutoDisposeFutureProvider.internal( - immichLogo, - name: r'immichLogoProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$immichLogoHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef ImmichLogoRef = AutoDisposeFutureProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index bad0d986d0..d0d1d5d424 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -3,29 +3,28 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/services/asset.service.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider; 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/providers/backup/asset_upload_progress.provider.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'; import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:logging/logging.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:openapi/api.dart'; -final actionProvider = NotifierProvider( - ActionNotifier.new, - dependencies: [multiSelectProvider, timelineServiceProvider], -); +final actionProvider = NotifierProvider(ActionNotifier.new, dependencies: [multiSelectProvider]); class ActionResult { final int count; @@ -490,6 +489,29 @@ class ActionNotifier extends Notifier { }); } } + + Future applyEdits(ActionSource source, List edits) async { + final ids = _getOwnedRemoteIdsForSource(source); + + if (ids.length != 1) { + _logger.warning('applyEdits called with multiple assets, expected single asset'); + return ActionResult(count: ids.length, success: false, error: 'Expected single asset for applying edits'); + } + + final completer = ref.read(websocketProvider.notifier).waitForEvent("AssetEditReadyV1", (dynamic data) { + final eventAsset = SyncAssetV1.fromJson(data["asset"]); + return eventAsset?.id == ids.first; + }, const Duration(seconds: 10)); + + try { + await _service.applyEdits(ids.first, edits); + await completer; + return const ActionResult(count: 1, success: true); + } catch (error, stack) { + _logger.severe('Failed to apply edits to assets', error, stack); + return ActionResult(count: ids.length, success: false, error: error.toString()); + } + } } extension on Iterable { diff --git a/mobile/lib/providers/infrastructure/db.provider.dart b/mobile/lib/providers/infrastructure/db.provider.dart index d38bcbfb55..2b4ba0129f 100644 --- a/mobile/lib/providers/infrastructure/db.provider.dart +++ b/mobile/lib/providers/infrastructure/db.provider.dart @@ -2,13 +2,6 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'db.provider.g.dart'; - -@Riverpod(keepAlive: true) -Isar isar(Ref ref) => throw UnimplementedError('isar'); Drift Function(Ref ref) driftOverride(Drift drift) => (ref) { ref.onDispose(() => unawaited(drift.close())); diff --git a/mobile/lib/providers/infrastructure/db.provider.g.dart b/mobile/lib/providers/infrastructure/db.provider.g.dart deleted file mode 100644 index 46abfb66a9..0000000000 --- a/mobile/lib/providers/infrastructure/db.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'db.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$isarHash() => r'69d3a06aa7e69a4381478e03f7956eb07d7f7feb'; - -/// See also [isar]. -@ProviderFor(isar) -final isarProvider = Provider.internal( - isar, - name: r'isarProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$isarHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef IsarRef = ProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/infrastructure/device_asset.provider.dart b/mobile/lib/providers/infrastructure/device_asset.provider.dart deleted file mode 100644 index 7854af016a..0000000000 --- a/mobile/lib/providers/infrastructure/device_asset.provider.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; - -final deviceAssetRepositoryProvider = Provider( - (ref) => IsarDeviceAssetRepository(ref.watch(isarProvider)), -); diff --git a/mobile/lib/providers/infrastructure/exif.provider.dart b/mobile/lib/providers/infrastructure/exif.provider.dart deleted file mode 100644 index c126f6cac0..0000000000 --- a/mobile/lib/providers/infrastructure/exif.provider.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'exif.provider.g.dart'; - -@Riverpod(keepAlive: true) -IsarExifRepository exifRepository(Ref ref) => IsarExifRepository(ref.watch(isarProvider)); diff --git a/mobile/lib/providers/infrastructure/exif.provider.g.dart b/mobile/lib/providers/infrastructure/exif.provider.g.dart deleted file mode 100644 index 0261558707..0000000000 --- a/mobile/lib/providers/infrastructure/exif.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'exif.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$exifRepositoryHash() => r'bf4a3f6a50d954a23d317659b4f3e2f381066463'; - -/// See also [exifRepository]. -@ProviderFor(exifRepository) -final exifRepositoryProvider = Provider.internal( - exifRepository, - name: r'exifRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$exifRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef ExifRepositoryRef = ProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/infrastructure/memory.provider.dart b/mobile/lib/providers/infrastructure/memory.provider.dart index 6fc75b8e6a..91495bb5ee 100644 --- a/mobile/lib/providers/infrastructure/memory.provider.dart +++ b/mobile/lib/providers/infrastructure/memory.provider.dart @@ -1,9 +1,9 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/services/memory.service.dart'; import 'package:immich_mobile/infrastructure/repositories/memory.repository.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; final driftMemoryRepositoryProvider = Provider( (ref) => DriftMemoryRepository(ref.watch(driftProvider)), diff --git a/mobile/lib/providers/infrastructure/partner.provider.dart b/mobile/lib/providers/infrastructure/partner.provider.dart index f4ba4cc73a..ac3d74d85b 100644 --- a/mobile/lib/providers/infrastructure/partner.provider.dart +++ b/mobile/lib/providers/infrastructure/partner.provider.dart @@ -1,9 +1,8 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/partner.service.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; class PartnerNotifier extends Notifier> { late DriftPartnerService _driftPartnerService; diff --git a/mobile/lib/providers/infrastructure/readonly_mode.provider.dart b/mobile/lib/providers/infrastructure/readonly_mode.provider.dart index 9e96c3cfc4..d503919c90 100644 --- a/mobile/lib/providers/infrastructure/readonly_mode.provider.dart +++ b/mobile/lib/providers/infrastructure/readonly_mode.provider.dart @@ -1,5 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; @@ -14,10 +15,11 @@ class ReadOnlyModeNotifier extends Notifier { } void setMode(bool value) { + final isLoggedIn = ref.read(authProvider).isAuthenticated; _appSettingService.setSetting(AppSettingsEnum.readonlyModeEnabled, value); state = value; - if (value) { + if (value && isLoggedIn) { ref.read(appRouterProvider).navigate(const MainTimelineRoute()); } } diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index 606ce3f129..3c00e2732c 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -8,7 +8,6 @@ import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:logging/logging.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; class RemoteAlbumState { final List albums; diff --git a/mobile/lib/providers/infrastructure/store.provider.dart b/mobile/lib/providers/infrastructure/store.provider.dart index 0bf42f3e8b..ba4d045b06 100644 --- a/mobile/lib/providers/infrastructure/store.provider.dart +++ b/mobile/lib/providers/infrastructure/store.provider.dart @@ -1,13 +1,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'store.provider.g.dart'; - -@Riverpod(keepAlive: true) -IsarStoreRepository storeRepository(Ref ref) => IsarStoreRepository(ref.watch(isarProvider)); - -@Riverpod(keepAlive: true) -StoreService storeService(Ref _) => StoreService.I; +final storeServiceProvider = Provider((_) => StoreService.I); diff --git a/mobile/lib/providers/infrastructure/store.provider.g.dart b/mobile/lib/providers/infrastructure/store.provider.g.dart deleted file mode 100644 index 98c978cb60..0000000000 --- a/mobile/lib/providers/infrastructure/store.provider.g.dart +++ /dev/null @@ -1,44 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'store.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$storeRepositoryHash() => r'659cb134466e4b0d5f04e2fc93e426350d99545f'; - -/// See also [storeRepository]. -@ProviderFor(storeRepository) -final storeRepositoryProvider = Provider.internal( - storeRepository, - name: r'storeRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$storeRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef StoreRepositoryRef = ProviderRef; -String _$storeServiceHash() => r'250e10497c42df360e9e1f9a618d0b19c1b5b0a0'; - -/// See also [storeService]. -@ProviderFor(storeService) -final storeServiceProvider = Provider.internal( - storeService, - name: r'storeServiceProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$storeServiceHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef StoreServiceRef = ProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/infrastructure/user.provider.dart b/mobile/lib/providers/infrastructure/user.provider.dart index 922b9866bb..d8e7029f8c 100644 --- a/mobile/lib/providers/infrastructure/user.provider.dart +++ b/mobile/lib/providers/infrastructure/user.provider.dart @@ -3,28 +3,20 @@ import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/partner.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/partner.provider.dart'; import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'user.provider.g.dart'; +final userApiRepositoryProvider = Provider((ref) => UserApiRepository(ref.watch(apiServiceProvider).usersApi)); -@Riverpod(keepAlive: true) -IsarUserRepository userRepository(Ref ref) => IsarUserRepository(ref.watch(isarProvider)); - -@Riverpod(keepAlive: true) -UserApiRepository userApiRepository(Ref ref) => UserApiRepository(ref.watch(apiServiceProvider).usersApi); - -@Riverpod(keepAlive: true) -UserService userService(Ref ref) => UserService( - isarUserRepository: ref.watch(userRepositoryProvider), - userApiRepository: ref.watch(userApiRepositoryProvider), - storeService: ref.watch(storeServiceProvider), +final userServiceProvider = Provider( + (ref) => UserService( + userApiRepository: ref.watch(userApiRepositoryProvider), + storeService: ref.watch(storeServiceProvider), + ), ); /// Drifts diff --git a/mobile/lib/providers/infrastructure/user.provider.g.dart b/mobile/lib/providers/infrastructure/user.provider.g.dart deleted file mode 100644 index f9148bf3a7..0000000000 --- a/mobile/lib/providers/infrastructure/user.provider.g.dart +++ /dev/null @@ -1,61 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'user.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$userRepositoryHash() => r'538791a4ad126ed086c9db682c67fc5c654d54f3'; - -/// See also [userRepository]. -@ProviderFor(userRepository) -final userRepositoryProvider = Provider.internal( - userRepository, - name: r'userRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$userRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef UserRepositoryRef = ProviderRef; -String _$userApiRepositoryHash() => r'8a7340ca4544c8c6b20225c65bff2abb9e96baa2'; - -/// See also [userApiRepository]. -@ProviderFor(userApiRepository) -final userApiRepositoryProvider = Provider.internal( - userApiRepository, - name: r'userApiRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$userApiRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef UserApiRepositoryRef = ProviderRef; -String _$userServiceHash() => r'181414dddc7891be6237e13d568c287a804228d1'; - -/// See also [userService]. -@ProviderFor(userService) -final userServiceProvider = Provider.internal( - userService, - name: r'userServiceProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$userServiceHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef UserServiceRef = ProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/map/map_marker.provider.dart b/mobile/lib/providers/map/map_marker.provider.dart index e107dd3602..38432eab6b 100644 --- a/mobile/lib/providers/map/map_marker.provider.dart +++ b/mobile/lib/providers/map/map_marker.provider.dart @@ -2,12 +2,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/map/map_marker.model.dart'; import 'package:immich_mobile/providers/map/map_service.provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'map_marker.provider.g.dart'; - -@riverpod -Future> mapMarkers(Ref ref) async { +final mapMarkersProvider = FutureProvider.autoDispose>((ref) async { final service = ref.read(mapServiceProvider); final mapState = ref.read(mapStateNotifierProvider); DateTime? fileCreatedAfter; @@ -31,4 +27,4 @@ Future> mapMarkers(Ref ref) async { ); return markers.toList(); -} +}); diff --git a/mobile/lib/providers/map/map_marker.provider.g.dart b/mobile/lib/providers/map/map_marker.provider.g.dart deleted file mode 100644 index 80a21a39b2..0000000000 --- a/mobile/lib/providers/map/map_marker.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'map_marker.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$mapMarkersHash() => r'a0c129fcddbf1b9bce4aafcd2e47a858ab6ef1c9'; - -/// See also [mapMarkers]. -@ProviderFor(mapMarkers) -final mapMarkersProvider = AutoDisposeFutureProvider>.internal( - mapMarkers, - name: r'mapMarkersProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$mapMarkersHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef MapMarkersRef = AutoDisposeFutureProviderRef>; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/map/map_service.provider.dart b/mobile/lib/providers/map/map_service.provider.dart index 4ae199789f..a1d47746e0 100644 --- a/mobile/lib/providers/map/map_service.provider.dart +++ b/mobile/lib/providers/map/map_service.provider.dart @@ -1,9 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/services/map.service.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:immich_mobile/services/map.service.dart'; -part 'map_service.provider.g.dart'; - -@riverpod -MapService mapService(Ref ref) => MapService(ref.watch(apiServiceProvider)); +final mapServiceProvider = Provider.autoDispose((ref) => MapService(ref.watch(apiServiceProvider))); diff --git a/mobile/lib/providers/map/map_service.provider.g.dart b/mobile/lib/providers/map/map_service.provider.g.dart deleted file mode 100644 index e8eb1cd1ee..0000000000 --- a/mobile/lib/providers/map/map_service.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'map_service.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$mapServiceHash() => r'ffc8f38b726083452b9df236ed58903879348987'; - -/// See also [mapService]. -@ProviderFor(mapService) -final mapServiceProvider = AutoDisposeProvider.internal( - mapService, - name: r'mapServiceProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$mapServiceHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef MapServiceRef = AutoDisposeProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/map/map_state.provider.dart b/mobile/lib/providers/map/map_state.provider.dart index 31f2849df6..63b277ac83 100644 --- a/mobile/lib/providers/map/map_state.provider.dart +++ b/mobile/lib/providers/map/map_state.provider.dart @@ -1,14 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/map/map_state.model.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'map_state.provider.g.dart'; +final mapStateNotifierProvider = NotifierProvider(MapStateNotifier.new); -@Riverpod(keepAlive: true) -class MapStateNotifier extends _$MapStateNotifier { +class MapStateNotifier extends Notifier { @override MapState build() { final appSettingsProvider = ref.read(appSettingsServiceProvider); diff --git a/mobile/lib/providers/map/map_state.provider.g.dart b/mobile/lib/providers/map/map_state.provider.g.dart deleted file mode 100644 index 94d0ff8698..0000000000 --- a/mobile/lib/providers/map/map_state.provider.g.dart +++ /dev/null @@ -1,26 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'map_state.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$mapStateNotifierHash() => r'22e4e571bd0730dbc34b109255a62b920e9c7d66'; - -/// See also [MapStateNotifier]. -@ProviderFor(MapStateNotifier) -final mapStateNotifierProvider = - NotifierProvider.internal( - MapStateNotifier.new, - name: r'mapStateNotifierProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$mapStateNotifierHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$MapStateNotifier = Notifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/memory.provider.dart b/mobile/lib/providers/memory.provider.dart deleted file mode 100644 index 7fef3060cc..0000000000 --- a/mobile/lib/providers/memory.provider.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/services/memory.service.dart'; - -final memoryFutureProvider = FutureProvider.autoDispose?>((ref) async { - final service = ref.watch(memoryServiceProvider); - - return await service.getMemoryLane(); -}); diff --git a/mobile/lib/providers/partner.provider.dart b/mobile/lib/providers/partner.provider.dart deleted file mode 100644 index 5a85cea1d4..0000000000 --- a/mobile/lib/providers/partner.provider.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/services/partner.service.dart'; - -class PartnerSharedWithNotifier extends StateNotifier> { - final PartnerService _partnerService; - late final StreamSubscription> streamSub; - - PartnerSharedWithNotifier(this._partnerService) : super([]) { - Function eq = const ListEquality().equals; - _partnerService - .getSharedWith() - .then((partners) { - if (!eq(state, partners)) { - state = partners; - } - }) - .then((_) { - streamSub = _partnerService.watchSharedWith().listen((partners) { - if (!eq(state, partners)) { - state = partners; - } - }); - }); - } - - Future updatePartner(UserDto partner, {required bool inTimeline}) { - return _partnerService.updatePartner(partner, inTimeline: inTimeline); - } - - @override - void dispose() { - if (mounted) { - streamSub.cancel(); - } - super.dispose(); - } -} - -final partnerSharedWithProvider = StateNotifierProvider>((ref) { - return PartnerSharedWithNotifier(ref.watch(partnerServiceProvider)); -}); - -class PartnerSharedByNotifier extends StateNotifier> { - final PartnerService _partnerService; - late final StreamSubscription> streamSub; - - PartnerSharedByNotifier(this._partnerService) : super([]) { - Function eq = const ListEquality().equals; - _partnerService - .getSharedBy() - .then((partners) { - if (!eq(state, partners)) { - state = partners; - } - }) - .then((_) { - streamSub = _partnerService.watchSharedBy().listen((partners) { - if (!eq(state, partners)) { - state = partners; - } - }); - }); - } - - @override - void dispose() { - if (mounted) { - streamSub.cancel(); - } - super.dispose(); - } -} - -final partnerSharedByProvider = StateNotifierProvider>((ref) { - return PartnerSharedByNotifier(ref.watch(partnerServiceProvider)); -}); - -final partnerAvailableProvider = FutureProvider.autoDispose>((ref) async { - final otherUsers = await ref.watch(otherUsersProvider.future); - final currentPartners = ref.watch(partnerSharedByProvider); - final available = Set.of(otherUsers); - available.removeAll(currentPartners); - return available.toList(); -}); diff --git a/mobile/lib/providers/search/all_motion_photos.provider.dart b/mobile/lib/providers/search/all_motion_photos.provider.dart deleted file mode 100644 index 48bc1bb80c..0000000000 --- a/mobile/lib/providers/search/all_motion_photos.provider.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; - -final allMotionPhotosProvider = FutureProvider>((ref) async { - return ref.watch(assetServiceProvider).getMotionAssets(); -}); diff --git a/mobile/lib/providers/search/paginated_search.provider.dart b/mobile/lib/providers/search/paginated_search.provider.dart deleted file mode 100644 index 9a37d83320..0000000000 --- a/mobile/lib/providers/search/paginated_search.provider.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/search/search_result.model.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/services/search.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'paginated_search.provider.g.dart'; - -final paginatedSearchProvider = StateNotifierProvider( - (ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)), -); - -class PaginatedSearchNotifier extends StateNotifier { - final SearchService _searchService; - - PaginatedSearchNotifier(this._searchService) : super(const SearchResult(assets: [], nextPage: 1)); - - Future search(SearchFilter filter) async { - if (state.nextPage == null) { - return false; - } - - final result = await _searchService.search(filter, state.nextPage!); - - if (result == null) { - return false; - } - - state = SearchResult(assets: [...state.assets, ...result.assets], nextPage: result.nextPage); - - return true; - } - - clear() { - state = const SearchResult(assets: [], nextPage: 1); - } -} - -@riverpod -Future paginatedSearchRenderList(Ref ref) { - final result = ref.watch(paginatedSearchProvider); - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.getTimelineFromAssets(result.assets, GroupAssetsBy.none); -} diff --git a/mobile/lib/providers/search/paginated_search.provider.g.dart b/mobile/lib/providers/search/paginated_search.provider.g.dart deleted file mode 100644 index e984997967..0000000000 --- a/mobile/lib/providers/search/paginated_search.provider.g.dart +++ /dev/null @@ -1,29 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'paginated_search.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$paginatedSearchRenderListHash() => - r'22d715ff7864e5a946be38322ce7813616f899c2'; - -/// See also [paginatedSearchRenderList]. -@ProviderFor(paginatedSearchRenderList) -final paginatedSearchRenderListProvider = - AutoDisposeFutureProvider.internal( - paginatedSearchRenderList, - name: r'paginatedSearchRenderListProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$paginatedSearchRenderListHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef PaginatedSearchRenderListRef = AutoDisposeFutureProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart index 3ff8d67983..1bd58509f5 100644 --- a/mobile/lib/providers/search/people.provider.dart +++ b/mobile/lib/providers/search/people.provider.dart @@ -1,40 +1,24 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/services/person.service.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'people.provider.g.dart'; - -@riverpod -Future> getAllPeople(Ref ref) async { +final getAllPeopleProvider = FutureProvider.autoDispose>((ref) async { final PersonService personService = ref.read(personServiceProvider); final people = await personService.getAllPeople(); return people; -} +}); -@riverpod -Future personAssets(Ref ref, String personId) async { - final PersonService personService = ref.read(personServiceProvider); - final assets = await personService.getPersonAssets(personId); +final updatePersonNameProvider = FutureProvider.autoDispose( + (ref) => (String personId, String updatedName) async { + final PersonService personService = ref.read(personServiceProvider); + final person = await personService.updateName(personId, updatedName); - final settings = ref.read(appSettingsServiceProvider); - final groupBy = GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; - return await RenderList.fromAssets(assets, groupBy); -} - -@riverpod -Future updatePersonName(Ref ref, String personId, String updatedName) async { - final PersonService personService = ref.read(personServiceProvider); - final person = await personService.updateName(personId, updatedName); - - if (person != null && person.name == updatedName) { - ref.invalidate(getAllPeopleProvider); - return true; - } - return false; -} + if (person != null && person.name == updatedName) { + ref.invalidate(getAllPeopleProvider); + return true; + } + return false; + }, +); diff --git a/mobile/lib/providers/search/people.provider.g.dart b/mobile/lib/providers/search/people.provider.g.dart deleted file mode 100644 index 9595c36eec..0000000000 --- a/mobile/lib/providers/search/people.provider.g.dart +++ /dev/null @@ -1,302 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'people.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$getAllPeopleHash() => r'2c5e6a207683f15ab209650615fdf9cb7f76c736'; - -/// See also [getAllPeople]. -@ProviderFor(getAllPeople) -final getAllPeopleProvider = - AutoDisposeFutureProvider>.internal( - getAllPeople, - name: r'getAllPeopleProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$getAllPeopleHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; -String _$personAssetsHash() => r'c1d35ee0e024bd6915e21bc724be4b458a14bc24'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -/// See also [personAssets]. -@ProviderFor(personAssets) -const personAssetsProvider = PersonAssetsFamily(); - -/// See also [personAssets]. -class PersonAssetsFamily extends Family> { - /// See also [personAssets]. - const PersonAssetsFamily(); - - /// See also [personAssets]. - PersonAssetsProvider call(String personId) { - return PersonAssetsProvider(personId); - } - - @override - PersonAssetsProvider getProviderOverride( - covariant PersonAssetsProvider provider, - ) { - return call(provider.personId); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'personAssetsProvider'; -} - -/// See also [personAssets]. -class PersonAssetsProvider extends AutoDisposeFutureProvider { - /// See also [personAssets]. - PersonAssetsProvider(String personId) - : this._internal( - (ref) => personAssets(ref as PersonAssetsRef, personId), - from: personAssetsProvider, - name: r'personAssetsProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$personAssetsHash, - dependencies: PersonAssetsFamily._dependencies, - allTransitiveDependencies: - PersonAssetsFamily._allTransitiveDependencies, - personId: personId, - ); - - PersonAssetsProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.personId, - }) : super.internal(); - - final String personId; - - @override - Override overrideWith( - FutureOr Function(PersonAssetsRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: PersonAssetsProvider._internal( - (ref) => create(ref as PersonAssetsRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - personId: personId, - ), - ); - } - - @override - AutoDisposeFutureProviderElement createElement() { - return _PersonAssetsProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is PersonAssetsProvider && other.personId == personId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, personId.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin PersonAssetsRef on AutoDisposeFutureProviderRef { - /// The parameter `personId` of this provider. - String get personId; -} - -class _PersonAssetsProviderElement - extends AutoDisposeFutureProviderElement - with PersonAssetsRef { - _PersonAssetsProviderElement(super.provider); - - @override - String get personId => (origin as PersonAssetsProvider).personId; -} - -String _$updatePersonNameHash() => r'45f7693172de522a227406d8198811434cf2bbbc'; - -/// See also [updatePersonName]. -@ProviderFor(updatePersonName) -const updatePersonNameProvider = UpdatePersonNameFamily(); - -/// See also [updatePersonName]. -class UpdatePersonNameFamily extends Family> { - /// See also [updatePersonName]. - const UpdatePersonNameFamily(); - - /// See also [updatePersonName]. - UpdatePersonNameProvider call(String personId, String updatedName) { - return UpdatePersonNameProvider(personId, updatedName); - } - - @override - UpdatePersonNameProvider getProviderOverride( - covariant UpdatePersonNameProvider provider, - ) { - return call(provider.personId, provider.updatedName); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'updatePersonNameProvider'; -} - -/// See also [updatePersonName]. -class UpdatePersonNameProvider extends AutoDisposeFutureProvider { - /// See also [updatePersonName]. - UpdatePersonNameProvider(String personId, String updatedName) - : this._internal( - (ref) => - updatePersonName(ref as UpdatePersonNameRef, personId, updatedName), - from: updatePersonNameProvider, - name: r'updatePersonNameProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$updatePersonNameHash, - dependencies: UpdatePersonNameFamily._dependencies, - allTransitiveDependencies: - UpdatePersonNameFamily._allTransitiveDependencies, - personId: personId, - updatedName: updatedName, - ); - - UpdatePersonNameProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.personId, - required this.updatedName, - }) : super.internal(); - - final String personId; - final String updatedName; - - @override - Override overrideWith( - FutureOr Function(UpdatePersonNameRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: UpdatePersonNameProvider._internal( - (ref) => create(ref as UpdatePersonNameRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - personId: personId, - updatedName: updatedName, - ), - ); - } - - @override - AutoDisposeFutureProviderElement createElement() { - return _UpdatePersonNameProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is UpdatePersonNameProvider && - other.personId == personId && - other.updatedName == updatedName; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, personId.hashCode); - hash = _SystemHash.combine(hash, updatedName.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin UpdatePersonNameRef on AutoDisposeFutureProviderRef { - /// The parameter `personId` of this provider. - String get personId; - - /// The parameter `updatedName` of this provider. - String get updatedName; -} - -class _UpdatePersonNameProviderElement - extends AutoDisposeFutureProviderElement - with UpdatePersonNameRef { - _UpdatePersonNameProviderElement(super.provider); - - @override - String get personId => (origin as UpdatePersonNameProvider).personId; - @override - String get updatedName => (origin as UpdatePersonNameProvider).updatedName; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/search/recently_taken_asset.provider.dart b/mobile/lib/providers/search/recently_taken_asset.provider.dart deleted file mode 100644 index 157e7c2a74..0000000000 --- a/mobile/lib/providers/search/recently_taken_asset.provider.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; - -final recentlyTakenAssetProvider = FutureProvider>((ref) async { - final assetService = ref.read(assetServiceProvider); - - return assetService.getRecentlyTakenAssets(); -}); diff --git a/mobile/lib/providers/search/search_filter.provider.dart b/mobile/lib/providers/search/search_filter.provider.dart index 2a81060522..3040ecd808 100644 --- a/mobile/lib/providers/search/search_filter.provider.dart +++ b/mobile/lib/providers/search/search_filter.provider.dart @@ -1,28 +1,47 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/services/search.service.dart'; import 'package:openapi/api.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'search_filter.provider.g.dart'; +class SearchSuggestionArgs { + SearchSuggestionType type; + final String? locationCountry; + final String? locationState; + final String? make; + final String? model; -@riverpod -Future> getSearchSuggestions( - Ref ref, - SearchSuggestionType type, { - String? locationCountry, - String? locationState, - String? make, - String? model, -}) async { + SearchSuggestionArgs({required this.type, this.locationCountry, this.locationState, this.make, this.model}); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is SearchSuggestionArgs && + other.type == type && + other.locationCountry == locationCountry && + other.locationState == locationState && + other.make == make && + other.model == model; + } + + @override + int get hashCode { + return type.hashCode ^ locationCountry.hashCode ^ locationState.hashCode ^ make.hashCode ^ model.hashCode; + } +} + +final getSearchSuggestionsProvider = FutureProvider.autoDispose.family, SearchSuggestionArgs>(( + ref, + args, +) async { final SearchService service = ref.read(searchServiceProvider); final suggestions = await service.getSearchSuggestions( - type, - country: locationCountry, - state: locationState, - make: make, - model: model, + args.type, + country: args.locationCountry, + state: args.locationState, + make: args.make, + model: args.model, ); return suggestions ?? []; -} +}); diff --git a/mobile/lib/providers/search/search_filter.provider.g.dart b/mobile/lib/providers/search/search_filter.provider.g.dart deleted file mode 100644 index 5a322ca285..0000000000 --- a/mobile/lib/providers/search/search_filter.provider.g.dart +++ /dev/null @@ -1,231 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'search_filter.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$getSearchSuggestionsHash() => - r'bc30a65e8fcb273cbd07bab876baf67bcc794737'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -/// See also [getSearchSuggestions]. -@ProviderFor(getSearchSuggestions) -const getSearchSuggestionsProvider = GetSearchSuggestionsFamily(); - -/// See also [getSearchSuggestions]. -class GetSearchSuggestionsFamily extends Family>> { - /// See also [getSearchSuggestions]. - const GetSearchSuggestionsFamily(); - - /// See also [getSearchSuggestions]. - GetSearchSuggestionsProvider call( - SearchSuggestionType type, { - String? locationCountry, - String? locationState, - String? make, - String? model, - }) { - return GetSearchSuggestionsProvider( - type, - locationCountry: locationCountry, - locationState: locationState, - make: make, - model: model, - ); - } - - @override - GetSearchSuggestionsProvider getProviderOverride( - covariant GetSearchSuggestionsProvider provider, - ) { - return call( - provider.type, - locationCountry: provider.locationCountry, - locationState: provider.locationState, - make: provider.make, - model: provider.model, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'getSearchSuggestionsProvider'; -} - -/// See also [getSearchSuggestions]. -class GetSearchSuggestionsProvider - extends AutoDisposeFutureProvider> { - /// See also [getSearchSuggestions]. - GetSearchSuggestionsProvider( - SearchSuggestionType type, { - String? locationCountry, - String? locationState, - String? make, - String? model, - }) : this._internal( - (ref) => getSearchSuggestions( - ref as GetSearchSuggestionsRef, - type, - locationCountry: locationCountry, - locationState: locationState, - make: make, - model: model, - ), - from: getSearchSuggestionsProvider, - name: r'getSearchSuggestionsProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$getSearchSuggestionsHash, - dependencies: GetSearchSuggestionsFamily._dependencies, - allTransitiveDependencies: - GetSearchSuggestionsFamily._allTransitiveDependencies, - type: type, - locationCountry: locationCountry, - locationState: locationState, - make: make, - model: model, - ); - - GetSearchSuggestionsProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.type, - required this.locationCountry, - required this.locationState, - required this.make, - required this.model, - }) : super.internal(); - - final SearchSuggestionType type; - final String? locationCountry; - final String? locationState; - final String? make; - final String? model; - - @override - Override overrideWith( - FutureOr> Function(GetSearchSuggestionsRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: GetSearchSuggestionsProvider._internal( - (ref) => create(ref as GetSearchSuggestionsRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - type: type, - locationCountry: locationCountry, - locationState: locationState, - make: make, - model: model, - ), - ); - } - - @override - AutoDisposeFutureProviderElement> createElement() { - return _GetSearchSuggestionsProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is GetSearchSuggestionsProvider && - other.type == type && - other.locationCountry == locationCountry && - other.locationState == locationState && - other.make == make && - other.model == model; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, type.hashCode); - hash = _SystemHash.combine(hash, locationCountry.hashCode); - hash = _SystemHash.combine(hash, locationState.hashCode); - hash = _SystemHash.combine(hash, make.hashCode); - hash = _SystemHash.combine(hash, model.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin GetSearchSuggestionsRef on AutoDisposeFutureProviderRef> { - /// The parameter `type` of this provider. - SearchSuggestionType get type; - - /// The parameter `locationCountry` of this provider. - String? get locationCountry; - - /// The parameter `locationState` of this provider. - String? get locationState; - - /// The parameter `make` of this provider. - String? get make; - - /// The parameter `model` of this provider. - String? get model; -} - -class _GetSearchSuggestionsProviderElement - extends AutoDisposeFutureProviderElement> - with GetSearchSuggestionsRef { - _GetSearchSuggestionsProviderElement(super.provider); - - @override - SearchSuggestionType get type => - (origin as GetSearchSuggestionsProvider).type; - @override - String? get locationCountry => - (origin as GetSearchSuggestionsProvider).locationCountry; - @override - String? get locationState => - (origin as GetSearchSuggestionsProvider).locationState; - @override - String? get make => (origin as GetSearchSuggestionsProvider).make; - @override - String? get model => (origin as GetSearchSuggestionsProvider).model; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/timeline.provider.dart b/mobile/lib/providers/timeline.provider.dart deleted file mode 100644 index 71ea308dbf..0000000000 --- a/mobile/lib/providers/timeline.provider.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/locale_provider.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; - -final singleUserTimelineProvider = StreamProvider.family((ref, userId) { - if (userId == null) { - return const Stream.empty(); - } - - ref.watch(localeProvider); - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchHomeTimeline(userId); -}, dependencies: [localeProvider]); - -final multiUsersTimelineProvider = StreamProvider.family>((ref, userIds) { - ref.watch(localeProvider); - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchMultiUsersTimeline(userIds); -}, dependencies: [localeProvider]); - -final albumTimelineProvider = StreamProvider.autoDispose.family((ref, id) { - final album = ref.watch(albumWatcher(id)).value; - final timelineService = ref.watch(timelineServiceProvider); - - if (album != null) { - return timelineService.watchAlbumTimeline(album); - } - - return const Stream.empty(); -}); - -final archiveTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchArchiveTimeline(); -}); - -final favoriteTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchFavoriteTimeline(); -}); - -final trashTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchTrashTimeline(); -}); - -final allVideosTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchAllVideosTimeline(); -}); - -final assetSelectionTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchAssetSelectionTimeline(); -}); - -final assetsTimelineProvider = FutureProvider.family>((ref, assets) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.getTimelineFromAssets(assets, null); -}); - -final lockedTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchLockedTimelineProvider(); -}); diff --git a/mobile/lib/providers/trash.provider.dart b/mobile/lib/providers/trash.provider.dart deleted file mode 100644 index 41b9160b9b..0000000000 --- a/mobile/lib/providers/trash.provider.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/trash.service.dart'; -import 'package:logging/logging.dart'; - -class TrashNotifier extends StateNotifier { - final TrashService _trashService; - final _log = Logger('TrashNotifier'); - - TrashNotifier(this._trashService) : super(false); - - Future emptyTrash() async { - try { - await _trashService.emptyTrash(); - state = true; - } catch (error, stack) { - _log.severe("Cannot empty trash", error, stack); - state = false; - } - } - - Future restoreAssets(Iterable assetList) async { - try { - await _trashService.restoreAssets(assetList); - return true; - } catch (error, stack) { - _log.severe("Cannot restore assets", error, stack); - return false; - } - } - - Future restoreTrash() async { - try { - await _trashService.restoreTrash(); - state = true; - } catch (error, stack) { - _log.severe("Cannot restore trash", error, stack); - state = false; - } - } -} - -final trashProvider = StateNotifierProvider((ref) { - return TrashNotifier(ref.watch(trashServiceProvider)); -}); diff --git a/mobile/lib/providers/user.provider.dart b/mobile/lib/providers/user.provider.dart index 10dcb2aff5..5a56b65793 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -1,11 +1,9 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; class CurrentUserProvider extends StateNotifier { CurrentUserProvider(this._userService) : super(null) { @@ -32,28 +30,3 @@ class CurrentUserProvider extends StateNotifier { final currentUserProvider = StateNotifierProvider((ref) { return CurrentUserProvider(ref.watch(userServiceProvider)); }); - -class TimelineUserIdsProvider extends StateNotifier> { - TimelineUserIdsProvider(this._timelineService) : super([]) { - final listEquality = const ListEquality(); - _timelineService.getTimelineUserIds().then((users) => state = users); - streamSub = _timelineService.watchTimelineUserIds().listen((users) { - if (!listEquality.equals(state, users)) { - state = users; - } - }); - } - - late final StreamSubscription> streamSub; - final TimelineService _timelineService; - - @override - void dispose() { - streamSub.cancel(); - super.dispose(); - } -} - -final timelineUsersIdsProvider = StateNotifierProvider>((ref) { - return TimelineUserIdsProvider(ref.watch(timelineServiceProvider)); -}); diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index 6643404786..c79f40a25d 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -1,60 +1,27 @@ import 'dart:async'; -import 'package:collection/collection.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'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; 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/db.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:socket_io_client/socket_io_client.dart'; -enum PendingAction { assetDelete, assetUploaded, assetHidden, assetTrash } - -class PendingChange { - final String id; - final PendingAction action; - final dynamic value; - - const PendingChange(this.id, this.action, this.value); - - @override - String toString() => 'PendingChange(id: $id, action: $action, value: $value)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is PendingChange && other.id == id && other.action == action; - } - - @override - int get hashCode => id.hashCode ^ action.hashCode; -} - class WebsocketState { final Socket? socket; final bool isConnected; - final List pendingChanges; - const WebsocketState({this.socket, required this.isConnected, required this.pendingChanges}); + const WebsocketState({this.socket, required this.isConnected}); - WebsocketState copyWith({Socket? socket, bool? isConnected, List? pendingChanges}) { - return WebsocketState( - socket: socket ?? this.socket, - isConnected: isConnected ?? this.isConnected, - pendingChanges: pendingChanges ?? this.pendingChanges, - ); + WebsocketState copyWith({Socket? socket, bool? isConnected}) { + return WebsocketState(socket: socket ?? this.socket, isConnected: isConnected ?? this.isConnected); } @override @@ -72,11 +39,10 @@ class WebsocketState { } class WebsocketNotifier extends StateNotifier { - WebsocketNotifier(this._ref) : super(const WebsocketState(socket: null, isConnected: false, pendingChanges: [])); + WebsocketNotifier(this._ref) : super(const WebsocketState(socket: null, isConnected: false)); final _log = Logger('WebsocketNotifier'); final Ref _ref; - final Debouncer _debounce = Debouncer(interval: const Duration(milliseconds: 500)); final Debouncer _batchDebouncer = Debouncer( interval: const Duration(seconds: 5), @@ -115,32 +81,21 @@ class WebsocketNotifier extends StateNotifier { socket.onConnect((_) { dPrint(() => "Established Websocket Connection"); - state = WebsocketState(isConnected: true, socket: socket, pendingChanges: state.pendingChanges); + state = WebsocketState(isConnected: true, socket: socket); }); socket.onDisconnect((_) { dPrint(() => "Disconnect to Websocket Connection"); - state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); + state = const WebsocketState(isConnected: false, socket: null); }); socket.on('error', (errorMessage) { _log.severe("Websocket Error - $errorMessage"); - state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); + state = const WebsocketState(isConnected: false, socket: null); }); - if (!Store.isBetaTimelineEnabled) { - socket.on('on_upload_success', _handleOnUploadSuccess); - socket.on('on_asset_delete', _handleOnAssetDelete); - socket.on('on_asset_trash', _handleOnAssetTrash); - socket.on('on_asset_restore', _handleServerUpdates); - socket.on('on_asset_update', _handleServerUpdates); - socket.on('on_asset_stack_update', _handleServerUpdates); - socket.on('on_asset_hidden', _handleOnAssetHidden); - } else { - socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); - socket.on('AssetEditReadyV1', _handleSyncAssetEditReady); - } - + socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); + socket.on('AssetEditReadyV1', _handleSyncAssetEditReady); socket.on('on_config_update', _handleOnConfigUpdate); socket.on('on_new_release', _handleReleaseUpdates); } catch (e) { @@ -155,109 +110,28 @@ class WebsocketNotifier extends StateNotifier { _batchedAssetUploadReady.clear(); state.socket?.dispose(); - state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); + state = const WebsocketState(isConnected: false, socket: null); } - void stopListenToEvent(String eventName) { - state.socket?.off(eventName); - } + Future waitForEvent(String event, bool Function(dynamic)? predicate, Duration timeout) { + final completer = Completer(); - void stopListenToOldEvents() { - state.socket?.off('on_upload_success'); - state.socket?.off('on_asset_delete'); - state.socket?.off('on_asset_trash'); - state.socket?.off('on_asset_restore'); - state.socket?.off('on_asset_update'); - state.socket?.off('on_asset_stack_update'); - state.socket?.off('on_asset_hidden'); - } - - void startListeningToOldEvents() { - state.socket?.on('on_upload_success', _handleOnUploadSuccess); - state.socket?.on('on_asset_delete', _handleOnAssetDelete); - state.socket?.on('on_asset_trash', _handleOnAssetTrash); - state.socket?.on('on_asset_restore', _handleServerUpdates); - state.socket?.on('on_asset_update', _handleServerUpdates); - state.socket?.on('on_asset_stack_update', _handleServerUpdates); - state.socket?.on('on_asset_hidden', _handleOnAssetHidden); - } - - void stopListeningToBetaEvents() { - state.socket?.off('AssetUploadReadyV1'); - state.socket?.off('AssetEditReadyV1'); - } - - void startListeningToBetaEvents() { - state.socket?.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); - state.socket?.on('AssetEditReadyV1', _handleSyncAssetEditReady); - } - - void listenUploadEvent() { - dPrint(() => "Start listening to event on_upload_success"); - state.socket?.on('on_upload_success', _handleOnUploadSuccess); - } - - void addPendingChange(PendingAction action, dynamic value) { - final now = DateTime.now(); - state = state.copyWith( - pendingChanges: [...state.pendingChanges, PendingChange(now.millisecondsSinceEpoch.toString(), action, value)], - ); - _debounce.run(handlePendingChanges); - } - - Future _handlePendingTrashes() async { - final trashChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetTrash).toList(); - if (trashChanges.isNotEmpty) { - List remoteIds = trashChanges.expand((a) => (a.value as List).map((e) => e.toString())).toList(); - - await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); - await _ref.read(assetProvider.notifier).getAllAsset(); - - state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => trashChanges.contains(c)).toList()); - } - } - - Future _handlePendingDeletes() async { - final deleteChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetDelete).toList(); - if (deleteChanges.isNotEmpty) { - List remoteIds = deleteChanges.map((a) => a.value.toString()).toList(); - await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); - state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => deleteChanges.contains(c)).toList()); - } - } - - Future _handlePendingUploaded() async { - final uploadedChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetUploaded).toList(); - if (uploadedChanges.isNotEmpty) { - List remoteAssets = uploadedChanges.map((a) => AssetResponseDto.fromJson(a.value)).toList(); - for (final dto in remoteAssets) { - if (dto != null) { - final newAsset = Asset.remote(dto); - await _ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset); - } + void handler(dynamic data) { + if (predicate == null || predicate(data)) { + completer.complete(); + state.socket?.off(event, handler); } - state = state.copyWith( - pendingChanges: state.pendingChanges.whereNot((c) => uploadedChanges.contains(c)).toList(), - ); } - } - Future _handlingPendingHidden() async { - final hiddenChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetHidden).toList(); - if (hiddenChanges.isNotEmpty) { - List remoteIds = hiddenChanges.map((a) => a.value.toString()).toList(); - final db = _ref.watch(dbProvider); - await db.writeTxn(() => db.assets.deleteAllByRemoteId(remoteIds)); + state.socket?.on(event, handler); - state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => hiddenChanges.contains(c)).toList()); - } - } - - Future handlePendingChanges() async { - await _handlePendingUploaded(); - await _handlePendingDeletes(); - await _handlingPendingHidden(); - await _handlePendingTrashes(); + return completer.future.timeout( + timeout, + onTimeout: () { + state.socket?.off(event, handler); + completer.completeError(TimeoutException("Timeout waiting for event: $event")); + }, + ); } void _handleOnConfigUpdate(dynamic _) { @@ -265,21 +139,6 @@ class WebsocketNotifier extends StateNotifier { _ref.read(serverInfoProvider.notifier).getServerConfig(); } - // Refresh updated assets - void _handleServerUpdates(dynamic _) { - _ref.read(assetProvider.notifier).getAllAsset(); - } - - void _handleOnUploadSuccess(dynamic data) => addPendingChange(PendingAction.assetUploaded, data); - - void _handleOnAssetDelete(dynamic data) => addPendingChange(PendingAction.assetDelete, data); - - void _handleOnAssetTrash(dynamic data) { - addPendingChange(PendingAction.assetTrash, data); - } - - void _handleOnAssetHidden(dynamic data) => addPendingChange(PendingAction.assetHidden, data); - _handleReleaseUpdates(dynamic data) { // Json guard if (data is! Map) { diff --git a/mobile/lib/repositories/album.repository.dart b/mobile/lib/repositories/album.repository.dart deleted file mode 100644 index 2d24004944..0000000000 --- a/mobile/lib/repositories/album.repository.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -enum AlbumSort { remoteId, localId } - -final albumRepositoryProvider = Provider((ref) => AlbumRepository(ref.watch(dbProvider))); - -class AlbumRepository extends DatabaseRepository { - const AlbumRepository(super.db); - - Future count({bool? local}) { - final baseQuery = db.albums.where(); - final QueryBuilder query = switch (local) { - null => baseQuery.noOp(), - true => baseQuery.localIdIsNotNull(), - false => baseQuery.remoteIdIsNotNull(), - }; - return query.count(); - } - - Future create(Album album) => txn(() => db.albums.store(album)); - - Future getByName(String name, {bool? shared, bool? remote, bool? owner}) { - var query = db.albums.filter().nameEqualTo(name); - if (shared != null) { - query = query.sharedEqualTo(shared); - } - final isarUserId = fastHash(Store.get(StoreKey.currentUser).id); - if (owner == true) { - query = query.owner((q) => q.isarIdEqualTo(isarUserId)); - } else if (owner == false) { - query = query.owner((q) => q.not().isarIdEqualTo(isarUserId)); - } - if (remote == true) { - query = query.localIdIsNull(); - } else if (remote == false) { - query = query.remoteIdIsNull(); - } - return query.findFirst(); - } - - Future update(Album album) => txn(() => db.albums.store(album)); - - Future delete(int albumId) => txn(() => db.albums.delete(albumId)); - - Future> getAll({bool? shared, bool? remote, int? ownerId, AlbumSort? sortBy}) { - final baseQuery = db.albums.where(); - final QueryBuilder afterWhere; - if (remote == null) { - afterWhere = baseQuery.noOp(); - } else if (remote) { - afterWhere = baseQuery.remoteIdIsNotNull(); - } else { - afterWhere = baseQuery.localIdIsNotNull(); - } - QueryBuilder filterQuery = afterWhere.filter().noOp(); - if (shared != null) { - filterQuery = filterQuery.sharedEqualTo(true); - } - if (ownerId != null) { - filterQuery = filterQuery.owner((q) => q.isarIdEqualTo(ownerId)); - } - final QueryBuilder query = switch (sortBy) { - null => filterQuery.noOp(), - AlbumSort.remoteId => filterQuery.sortByRemoteId(), - AlbumSort.localId => filterQuery.sortByLocalId(), - }; - return query.findAll(); - } - - Future get(int id) => db.albums.get(id); - - Future getByRemoteId(String remoteId) { - return db.albums.filter().remoteIdEqualTo(remoteId).findFirst(); - } - - Future removeUsers(Album album, List users) => - txn(() => album.sharedUsers.update(unlink: users.map(entity.User.fromDto))); - - Future addAssets(Album album, List assets) => txn(() => album.assets.update(link: assets)); - - Future removeAssets(Album album, List assets) => txn(() => album.assets.update(unlink: assets)); - - Future recalculateMetadata(Album album) async { - album.startDate = await album.assets.filter().fileCreatedAtProperty().min(); - album.endDate = await album.assets.filter().fileCreatedAtProperty().max(); - album.lastModifiedAssetTimestamp = await album.assets.filter().updatedAtProperty().max(); - return album; - } - - Future addUsers(Album album, List users) => - txn(() => album.sharedUsers.update(link: users.map(entity.User.fromDto))); - - Future deleteAllLocal() => txn(() => db.albums.where().localIdIsNotNull().deleteAll()); - - Future> search(String searchTerm, QuickFilterMode filterMode) async { - var query = db.albums.filter().nameContains(searchTerm, caseSensitive: false).remoteIdIsNotNull(); - final isarUserId = fastHash(Store.get(StoreKey.currentUser).id); - - switch (filterMode) { - case QuickFilterMode.sharedWithMe: - query = query.owner((q) => q.not().isarIdEqualTo(isarUserId)); - case QuickFilterMode.myAlbums: - query = query.owner((q) => q.isarIdEqualTo(isarUserId)); - case QuickFilterMode.all: - break; - } - - return await query.findAll(); - } - - Future clearTable() async { - await txn(() async { - await db.albums.clear(); - }); - } - - Stream> watchRemoteAlbums() { - return db.albums.where().remoteIdIsNotNull().watch(); - } - - Stream> watchLocalAlbums() { - return db.albums.where().localIdIsNotNull().watch(); - } - - Stream watchAlbum(int id) { - return db.albums.watchObject(id, fireImmediately: true); - } -} diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart deleted file mode 100644 index 525f0906ba..0000000000 --- a/mobile/lib/repositories/album_api.repository.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/album/album.model.dart' show AlbumAssetOrder, RemoteAlbum; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/repositories/api.repository.dart'; -import 'package:openapi/api.dart'; - -final albumApiRepositoryProvider = Provider((ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi)); - -class AlbumApiRepository extends ApiRepository { - final AlbumsApi _api; - - AlbumApiRepository(this._api); - - Future get(String id) async { - final dto = await checkNull(_api.getAlbumInfo(id)); - return _toAlbum(dto); - } - - Future> getAll({bool? shared}) async { - final dtos = await checkNull(_api.getAllAlbums(shared: shared)); - return dtos.map(_toAlbum).toList(); - } - - Future create( - String name, { - required Iterable assetIds, - Iterable sharedUserIds = const [], - String? description, - }) async { - final users = sharedUserIds.map((id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor)); - final responseDto = await checkNull( - _api.createAlbum( - CreateAlbumDto( - albumName: name, - description: description, - assetIds: assetIds.toList(), - albumUsers: users.toList(), - ), - ), - ); - return _toAlbum(responseDto); - } - - // TODO: Change name after removing old method - Future createDriftAlbum(String name, {required Iterable assetIds, String? description}) async { - final responseDto = await checkNull( - _api.createAlbum(CreateAlbumDto(albumName: name, description: description, assetIds: assetIds.toList())), - ); - - return _toRemoteAlbum(responseDto); - } - - Future update( - String albumId, { - String? name, - String? thumbnailAssetId, - String? description, - bool? activityEnabled, - SortOrder? sortOrder, - }) async { - AssetOrder? order; - if (sortOrder != null) { - order = sortOrder == SortOrder.asc ? AssetOrder.asc : AssetOrder.desc; - } - - final response = await checkNull( - _api.updateAlbumInfo( - albumId, - UpdateAlbumDto( - albumName: name, - albumThumbnailAssetId: thumbnailAssetId, - description: description, - isActivityEnabled: activityEnabled, - order: order, - ), - ), - ); - - return _toAlbum(response); - } - - Future delete(String albumId) { - return _api.deleteAlbum(albumId); - } - - Future<({List added, List duplicates})> addAssets(String albumId, Iterable assetIds) async { - final response = await checkNull(_api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); - - final List added = []; - final List duplicates = []; - - for (final result in response) { - if (result.success) { - added.add(result.id); - } else if (result.error == BulkIdResponseDtoErrorEnum.duplicate) { - duplicates.add(result.id); - } - } - return (added: added, duplicates: duplicates); - } - - Future<({List removed, List failed})> removeAssets(String albumId, Iterable assetIds) async { - final response = await checkNull(_api.removeAssetFromAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); - final List removed = [], failed = []; - for (final dto in response) { - if (dto.success) { - removed.add(dto.id); - } else { - failed.add(dto.id); - } - } - return (removed: removed, failed: failed); - } - - Future addUsers(String albumId, Iterable userIds) async { - final albumUsers = userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList(); - final response = await checkNull(_api.addUsersToAlbum(albumId, AddUsersDto(albumUsers: albumUsers))); - return _toAlbum(response); - } - - Future removeUser(String albumId, {required String userId}) { - return _api.removeUserFromAlbum(albumId, userId); - } - - static Album _toAlbum(AlbumResponseDto dto) { - final Album album = Album( - remoteId: dto.id, - name: dto.albumName, - createdAt: dto.createdAt, - modifiedAt: dto.updatedAt, - lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp, - shared: dto.shared, - startDate: dto.startDate, - description: dto.description, - endDate: dto.endDate, - activityEnabled: dto.isActivityEnabled, - sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc, - ); - album.remoteAssetCount = dto.assetCount; - album.owner.value = entity.User.fromDto(UserConverter.fromSimpleUserDto(dto.owner)); - album.remoteThumbnailAssetId = dto.albumThumbnailAssetId; - final users = dto.albumUsers.map((albumUser) => UserConverter.fromSimpleUserDto(albumUser.user)); - album.sharedUsers.addAll(users.map(entity.User.fromDto)); - final assets = dto.assets.map(Asset.remote).toList(); - album.assets.addAll(assets); - - return album; - } - - static RemoteAlbum _toRemoteAlbum(AlbumResponseDto dto) { - return RemoteAlbum( - id: dto.id, - name: dto.albumName, - ownerId: dto.owner.id, - description: dto.description, - createdAt: dto.createdAt, - updatedAt: dto.updatedAt, - thumbnailAssetId: dto.albumThumbnailAssetId, - isActivityEnabled: dto.isActivityEnabled, - order: dto.order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc, - assetCount: dto.assetCount, - ownerName: dto.owner.name, - isShared: dto.albumUsers.length > 2, - ); - } -} diff --git a/mobile/lib/repositories/album_media.repository.dart b/mobile/lib/repositories/album_media.repository.dart deleted file mode 100644 index 89860f4e75..0000000000 --- a/mobile/lib/repositories/album_media.repository.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:photo_manager/photo_manager.dart' hide AssetType; - -final albumMediaRepositoryProvider = Provider((ref) => const AlbumMediaRepository()); - -class AlbumMediaRepository { - const AlbumMediaRepository(); - - bool get useCustomFilter => Store.get(StoreKey.photoManagerCustomFilter, true); - - FilterOptionGroup? _getAlbumFilter({ - DateTimeCond? updateTimeCond, - bool? containsPathModified, - List? orderBy, - }) => useCustomFilter - ? FilterOptionGroup( - imageOption: const FilterOption(needTitle: true, sizeConstraint: SizeConstraint(ignoreSize: true)), - videoOption: const FilterOption( - needTitle: true, - sizeConstraint: SizeConstraint(ignoreSize: true), - durationConstraint: DurationConstraint(allowNullable: true), - ), - containsPathModified: containsPathModified ?? false, - createTimeCond: DateTimeCond.def().copyWith(ignore: true), - updateTimeCond: updateTimeCond ?? DateTimeCond.def().copyWith(ignore: true), - orders: orderBy ?? [], - ) - : null; - - Future> getAll() async { - final filter = useCustomFilter - ? CustomFilter.sql(where: '${CustomColumns.base.width} > 0') - : FilterOptionGroup(containsPathModified: true); - - final List assetPathEntities = await PhotoManager.getAssetPathList( - hasAll: true, - filterOption: filter, - ); - return assetPathEntities.map(_toAlbum).toList(); - } - - Future> getAssetIds(String albumId) async { - final album = await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter()); - final List assets = await album.getAssetListRange(start: 0, end: 0x7fffffffffffffff); - return assets.map((e) => e.id).toList(); - } - - Future getAssetCount(String albumId) async { - final album = await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter()); - return album.assetCountAsync; - } - - Future> getAssets( - String albumId, { - int start = 0, - int end = 0x7fffffffffffffff, - DateTime? modifiedFrom, - DateTime? modifiedUntil, - bool orderByModificationDate = false, - }) async { - final onDevice = await AssetPathEntity.fromId( - albumId, - filterOption: _getAlbumFilter( - updateTimeCond: modifiedFrom == null && modifiedUntil == null - ? null - : DateTimeCond(min: modifiedFrom ?? DateTime.utc(-271820), max: modifiedUntil ?? DateTime.utc(275760)), - orderBy: orderByModificationDate ? [const OrderOption(type: OrderOptionType.updateDate)] : [], - ), - ); - - final List assets = await onDevice.getAssetListRange(start: start, end: end); - return assets.map(AssetMediaRepository.toAsset).toList().cast(); - } - - Future get(String id) async { - final assetPathEntity = await AssetPathEntity.fromId(id, filterOption: _getAlbumFilter(containsPathModified: true)); - return _toAlbum(assetPathEntity); - } - - static Album _toAlbum(AssetPathEntity assetPathEntity) { - final Album album = Album( - name: assetPathEntity.name, - createdAt: assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(), - modifiedAt: assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(), - shared: false, - activityEnabled: false, - ); - album.owner.value = User.fromDto(Store.get(StoreKey.currentUser)); - album.localId = assetPathEntity.id; - album.isAll = assetPathEntity.isAll; - return album; - } -} diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart deleted file mode 100644 index 79af8b4921..0000000000 --- a/mobile/lib/repositories/asset.repository.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -enum AssetSort { checksum, ownerIdChecksum } - -final assetRepositoryProvider = Provider((ref) => AssetRepository(ref.watch(dbProvider))); - -class AssetRepository extends DatabaseRepository { - const AssetRepository(super.db); - - Future> getByAlbum( - Album album, { - Iterable notOwnedBy = const [], - String? ownerId, - AssetState? state, - AssetSort? sortBy, - }) { - var query = album.assets.filter(); - final isarUserIds = notOwnedBy.map(fastHash).toList(); - if (notOwnedBy.length == 1) { - query = query.not().ownerIdEqualTo(isarUserIds.first); - } else if (notOwnedBy.isNotEmpty) { - query = query.not().anyOf(isarUserIds, (q, int id) => q.ownerIdEqualTo(id)); - } - if (ownerId != null) { - query = query.ownerIdEqualTo(fastHash(ownerId)); - } - - if (state != null) { - query = switch (state) { - AssetState.local => query.remoteIdIsNull(), - AssetState.remote => query.localIdIsNull(), - AssetState.merged => query.localIdIsNotNull().remoteIdIsNotNull(), - }; - } - - final QueryBuilder sortedQuery = switch (sortBy) { - null => query.noOp(), - AssetSort.checksum => query.sortByChecksum(), - AssetSort.ownerIdChecksum => query.sortByOwnerId().thenByChecksum(), - }; - - return sortedQuery.findAll(); - } - - Future deleteByIds(List ids) => txn(() async { - await db.assets.deleteAll(ids); - await db.exifInfos.deleteAll(ids); - }); - - Future getByRemoteId(String id) => db.assets.getByRemoteId(id); - - Future> getAllByRemoteId(Iterable ids, {AssetState? state}) async { - if (ids.isEmpty) { - return []; - } - - return _getAllByRemoteIdImpl(ids, state).findAll(); - } - - QueryBuilder _getAllByRemoteIdImpl(Iterable ids, AssetState? state) { - final query = db.assets.remote(ids).filter(); - return switch (state) { - null => query.noOp(), - AssetState.local => query.remoteIdIsNull(), - AssetState.remote => query.localIdIsNull(), - AssetState.merged => query.localIdIsNotEmpty().remoteIdIsNotNull(), - }; - } - - Future> getAll({required String ownerId, AssetState? state, AssetSort? sortBy, int? limit}) { - final baseQuery = db.assets.where(); - final isarUserIds = fastHash(ownerId); - final QueryBuilder filteredQuery = switch (state) { - null => baseQuery.ownerIdEqualToAnyChecksum(isarUserIds).noOp(), - AssetState.local => baseQuery.remoteIdIsNull().filter().localIdIsNotNull().ownerIdEqualTo(isarUserIds), - AssetState.remote => baseQuery.localIdIsNull().filter().remoteIdIsNotNull().ownerIdEqualTo(isarUserIds), - AssetState.merged => - baseQuery.ownerIdEqualToAnyChecksum(isarUserIds).filter().remoteIdIsNotNull().localIdIsNotNull(), - }; - - final QueryBuilder query = switch (sortBy) { - null => filteredQuery.noOp(), - AssetSort.checksum => filteredQuery.sortByChecksum(), - AssetSort.ownerIdChecksum => filteredQuery.sortByOwnerId().thenByChecksum(), - }; - - return limit == null ? query.findAll() : query.limit(limit).findAll(); - } - - Future> updateAll(List assets) async { - await txn(() => db.assets.putAll(assets)); - return assets; - } - - Future> getMatches({ - required List assets, - required String ownerId, - AssetState? state, - int limit = 100, - }) { - final baseQuery = db.assets.where(); - final QueryBuilder query = switch (state) { - null => baseQuery.noOp(), - AssetState.local => baseQuery.remoteIdIsNull().filter().localIdIsNotNull(), - AssetState.remote => baseQuery.localIdIsNull().filter().remoteIdIsNotNull(), - AssetState.merged => baseQuery.localIdIsNotNull().filter().remoteIdIsNotNull(), - }; - return _getMatchesImpl(query, fastHash(ownerId), assets, limit); - } - - Future update(Asset asset) async { - await txn(() => asset.put(db)); - return asset; - } - - Future upsertDuplicatedAssets(Iterable duplicatedAssets) => - txn(() => db.duplicatedAssets.putAll(duplicatedAssets.map(DuplicatedAsset.new).toList())); - - Future> getAllDuplicatedAssetIds() => db.duplicatedAssets.where().idProperty().findAll(); - - Future getByOwnerIdChecksum(int ownerId, String checksum) => - db.assets.getByOwnerIdChecksum(ownerId, checksum); - - Future> getAllByOwnerIdChecksum(List ownerIds, List checksums) => - db.assets.getAllByOwnerIdChecksum(ownerIds, checksums); - - Future> getAllLocal() => db.assets.where().localIdIsNotNull().findAll(); - - Future deleteAllByRemoteId(List ids, {AssetState? state}) => - txn(() => _getAllByRemoteIdImpl(ids, state).deleteAll()); - - Future> getStackAssets(String stackId) { - return db.assets - .filter() - .isArchivedEqualTo(false) - .isTrashedEqualTo(false) - .stackIdEqualTo(stackId) - // orders primary asset first as its ID is null - .sortByStackPrimaryAssetId() - .thenByFileCreatedAtDesc() - .findAll(); - } - - Future clearTable() async { - await txn(() async { - await db.assets.clear(); - }); - } - - Stream watchAsset(int id, {bool fireImmediately = false}) { - return db.assets.watchObject(id, fireImmediately: fireImmediately); - } - - Future> getTrashAssets(String userId) { - return db.assets - .where() - .remoteIdIsNotNull() - .filter() - .ownerIdEqualTo(fastHash(userId)) - .isTrashedEqualTo(true) - .findAll(); - } - - Future> getRecentlyTakenAssets(String userId) { - return db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .sortByFileCreatedAtDesc() - .findAll(); - } - - Future> getMotionAssets(String userId) { - return db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .livePhotoVideoIdIsNotNull() - .findAll(); - } -} - -Future> _getMatchesImpl( - QueryBuilder query, - int ownerId, - List assets, - int limit, -) => query - .ownerIdEqualTo(ownerId) - .anyOf( - assets, - (q, Asset a) => q - .fileNameEqualTo(a.fileName) - .and() - .durationInSecondsEqualTo(a.durationInSeconds) - .and() - .fileCreatedAtBetween( - a.fileCreatedAt.subtract(const Duration(hours: 12)), - a.fileCreatedAt.add(const Duration(hours: 12)), - ) - .and() - .not() - .checksumEqualTo(a.checksum), - ) - .sortByFileName() - .thenByFileCreatedAt() - .thenByFileModifiedAt() - .limit(limit) - .findAll(); diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index 011b1edc94..2943177d60 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -1,8 +1,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart' hide AssetEditAction; import 'package:immich_mobile/domain/models/stack.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -11,7 +11,6 @@ import 'package:openapi/api.dart'; final assetApiRepositoryProvider = Provider( (ref) => AssetApiRepository( ref.watch(apiServiceProvider).assetsApi, - ref.watch(apiServiceProvider).searchApi, ref.watch(apiServiceProvider).stacksApi, ref.watch(apiServiceProvider).trashApi, ), @@ -19,32 +18,10 @@ final assetApiRepositoryProvider = Provider( class AssetApiRepository extends ApiRepository { final AssetsApi _api; - final SearchApi _searchApi; final StacksApi _stacksApi; final TrashApi _trashApi; - AssetApiRepository(this._api, this._searchApi, this._stacksApi, this._trashApi); - - Future update(String id, {String? description}) async { - final response = await checkNull(_api.updateAsset(id, UpdateAssetDto(description: description))); - return Asset.remote(response); - } - - Future> search({List personIds = const []}) async { - // TODO this always fetches all assets, change API and usage to actually do pagination - final List result = []; - bool hasNext = true; - int currentPage = 1; - while (hasNext) { - final response = await checkNull( - _searchApi.searchAssets(MetadataSearchDto(personIds: personIds, page: currentPage, size: 1000)), - ); - result.addAll(response.assets.items.map(Asset.remote)); - hasNext = response.assets.nextPage != null; - currentPage++; - } - return result; - } + AssetApiRepository(this._api, this._stacksApi, this._trashApi); Future delete(List ids, bool force) async { return _api.deleteAssets(AssetBulkDeleteDto(ids: ids, force: force)); @@ -105,6 +82,14 @@ class AssetApiRepository extends ApiRepository { Future updateRating(String assetId, int rating) { return _api.updateAsset(assetId, UpdateAssetDto(rating: rating)); } + + Future editAsset(String assetId, List edits) { + return _api.editAsset(assetId, AssetEditsCreateDto(edits: edits.map((e) => e.toApi()).toList())); + } + + Future removeEdits(String assetId) async { + return _api.removeAssetEdits(assetId); + } } extension on StackResponseDto { @@ -112,3 +97,22 @@ extension on StackResponseDto { return StackResponse(id: id, primaryAssetId: primaryAssetId, assetIds: assets.map((asset) => asset.id).toList()); } } + +extension on AssetEdit { + AssetEditActionItemDto toApi() { + return switch (this) { + CropEdit(:final parameters) => AssetEditActionItemDto( + action: AssetEditAction.crop, + parameters: parameters.toJson(), + ), + RotateEdit(:final parameters) => AssetEditActionItemDto( + action: AssetEditAction.rotate, + parameters: parameters.toJson(), + ), + MirrorEdit(:final parameters) => AssetEditActionItemDto( + action: AssetEditAction.mirror, + parameters: parameters.toJson(), + ), + }; + } +} diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index fecfe6df4d..a2d8bfe162 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -5,15 +5,10 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as asset_entity; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/response_extensions.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -50,39 +45,9 @@ class AssetMediaRepository { return PhotoManager.editor.deleteWithIds(ids); } - Future get(String id) async { + Future get(String id) async { final entity = await AssetEntity.fromId(id); - return toAsset(entity); - } - - static asset_entity.Asset? toAsset(AssetEntity? local) { - if (local == null) return null; - - final asset_entity.Asset asset = asset_entity.Asset( - checksum: "", - localId: local.id, - ownerId: fastHash(Store.get(StoreKey.currentUser).id), - fileCreatedAt: local.createDateTime, - fileModifiedAt: local.modifiedDateTime, - updatedAt: local.modifiedDateTime, - durationInSeconds: local.duration, - type: asset_entity.AssetType.values[local.typeInt], - fileName: local.title!, - width: local.width, - height: local.height, - isFavorite: local.isFavorite, - ); - - if (asset.fileCreatedAt.year == 1970) { - asset.fileCreatedAt = asset.fileModifiedAt; - } - - if (local.latitude != null) { - asset.exifInfo = ExifInfo(latitude: local.latitude, longitude: local.longitude); - } - - asset.local = local; - return asset; + return entity; } Future getOriginalFilename(String id) async { diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index a8544ef6c0..c16b728ae5 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -2,40 +2,21 @@ import 'dart:convert'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/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'; -import 'package:immich_mobile/repositories/database.repository.dart'; -final authRepositoryProvider = Provider( - (ref) => AuthRepository(ref.watch(dbProvider), ref.watch(driftProvider)), -); +final authRepositoryProvider = Provider((ref) => AuthRepository(ref.watch(driftProvider))); -class AuthRepository extends DatabaseRepository { +class AuthRepository { final Drift _drift; - const AuthRepository(super.db, this._drift); + const AuthRepository(this._drift); Future clearLocalData() async { await SyncStreamRepository(_drift).reset(); - - return db.writeTxn(() { - return Future.wait([ - db.assets.clear(), - db.exifInfos.clear(), - db.albums.clear(), - db.eTags.clear(), - db.users.clear(), - ]); - }); } bool getEndpointSwitchingFeature() { diff --git a/mobile/lib/repositories/backup.repository.dart b/mobile/lib/repositories/backup.repository.dart deleted file mode 100644 index 6cee6a4427..0000000000 --- a/mobile/lib/repositories/backup.repository.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -enum BackupAlbumSort { id } - -final backupAlbumRepositoryProvider = Provider((ref) => BackupAlbumRepository(ref.watch(dbProvider))); - -class BackupAlbumRepository extends DatabaseRepository { - const BackupAlbumRepository(super.db); - - Future> getAll({BackupAlbumSort? sort}) { - final baseQuery = db.backupAlbums.where(); - final QueryBuilder query = switch (sort) { - null => baseQuery.noOp(), - BackupAlbumSort.id => baseQuery.sortById(), - }; - return query.findAll(); - } - - Future> getIdsBySelection(BackupSelection backup) => - db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll(); - - Future> getAllBySelection(BackupSelection backup) => - db.backupAlbums.filter().selectionEqualTo(backup).findAll(); - - Future deleteAll(List ids) => txn(() => db.backupAlbums.deleteAll(ids)); - - Future updateAll(List backupAlbums) => txn(() => db.backupAlbums.putAll(backupAlbums)); -} diff --git a/mobile/lib/repositories/database.repository.dart b/mobile/lib/repositories/database.repository.dart deleted file mode 100644 index 71c15e1c40..0000000000 --- a/mobile/lib/repositories/database.repository.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:async'; -import 'package:immich_mobile/interfaces/database.interface.dart'; -import 'package:isar/isar.dart'; - -/// copied from Isar; needed to check if an async transaction is already active -const Symbol _zoneTxn = #zoneTxn; - -abstract class DatabaseRepository implements IDatabaseRepository { - final Isar db; - const DatabaseRepository(this.db); - - bool get inTxn => Zone.current[_zoneTxn] != null; - - Future txn(Future Function() callback) => inTxn ? callback() : transaction(callback); - - @override - Future transaction(Future Function() callback) => db.writeTxn(callback); -} - -extension Asd on QueryBuilder { - QueryBuilder noOp() { - // ignore: invalid_use_of_protected_member - return QueryBuilder.apply(this, (query) => query); - } -} diff --git a/mobile/lib/repositories/etag.repository.dart b/mobile/lib/repositories/etag.repository.dart deleted file mode 100644 index 768d95b95c..0000000000 --- a/mobile/lib/repositories/etag.repository.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -final etagRepositoryProvider = Provider((ref) => ETagRepository(ref.watch(dbProvider))); - -class ETagRepository extends DatabaseRepository { - const ETagRepository(super.db); - - Future> getAllIds() => db.eTags.where().idProperty().findAll(); - - Future get(String id) => db.eTags.getById(id); - - Future upsertAll(List etags) => txn(() => db.eTags.putAll(etags)); - - Future deleteByIds(List ids) => txn(() => db.eTags.deleteAllById(ids)); - - Future getById(String id) => db.eTags.getById(id); - - Future clearTable() async { - await txn(() async { - await db.eTags.clear(); - }); - } -} diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart index f5cdb6d5c0..c54813a757 100644 --- a/mobile/lib/repositories/file_media.repository.dart +++ b/mobile/lib/repositories/file_media.repository.dart @@ -3,18 +3,12 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' hide AssetType; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:photo_manager/photo_manager.dart' hide AssetType; final fileMediaRepositoryProvider = Provider((ref) => const FileMediaRepository()); class FileMediaRepository { const FileMediaRepository(); - Future saveImage(Uint8List data, {required String title, String? relativePath}) async { - final entity = await PhotoManager.editor.saveImage(data, filename: title, title: title, relativePath: relativePath); - return AssetMediaRepository.toAsset(entity); - } Future saveLocalAsset(Uint8List data, {required String title, String? relativePath}) async { final entity = await PhotoManager.editor.saveImage(data, filename: title, title: title, relativePath: relativePath); @@ -30,24 +24,18 @@ class FileMediaRepository { ); } - Future saveImageWithFile(String filePath, {String? title, String? relativePath}) async { + Future saveImageWithFile(String filePath, {String? title, String? relativePath}) async { final entity = await PhotoManager.editor.saveImageWithPath(filePath, title: title, relativePath: relativePath); - return AssetMediaRepository.toAsset(entity); + return entity; } - Future saveLivePhoto({required File image, required File video, required String title}) async { + Future saveLivePhoto({required File image, required File video, required String title}) async { final entity = await PhotoManager.editor.darwin.saveLivePhoto(imageFile: image, videoFile: video, title: title); - return AssetMediaRepository.toAsset(entity); + return entity; } - Future saveVideo(File file, {required String title, String? relativePath}) async { + Future saveVideo(File file, {required String title, String? relativePath}) async { final entity = await PhotoManager.editor.saveVideo(file, title: title, relativePath: relativePath); - return AssetMediaRepository.toAsset(entity); + return entity; } - - Future clearFileCache() => PhotoManager.clearFileCache(); - - Future enableBackgroundAccess() => PhotoManager.setIgnorePermissionCheck(true); - - Future requestExtendedPermissions() => PhotoManager.requestPermissionExtend(); } diff --git a/mobile/lib/repositories/folder_api.repository.dart b/mobile/lib/repositories/folder_api.repository.dart index d20ca8e0a9..8c9959389c 100644 --- a/mobile/lib/repositories/folder_api.repository.dart +++ b/mobile/lib/repositories/folder_api.repository.dart @@ -1,5 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/asset_extensions.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; import 'package:logging/logging.dart'; @@ -23,10 +24,10 @@ class FolderApiRepository extends ApiRepository { } } - Future> getAssetsForPath(String? path) async { + Future> getAssetsForPath(String? path) async { try { final list = await _api.getAssetsByOriginalPath(path ?? '/'); - return list != null ? list.map(Asset.remote).toList() : []; + return list != null ? list.map((e) => e.toDtoWithExif()).toList() : []; } catch (e, stack) { _log.severe("Failed to fetch Assets by original path", e, stack); return []; diff --git a/mobile/lib/repositories/partner.repository.dart b/mobile/lib/repositories/partner.repository.dart deleted file mode 100644 index 7f5ce62e0c..0000000000 --- a/mobile/lib/repositories/partner.repository.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -final partnerRepositoryProvider = Provider((ref) => PartnerRepository(ref.watch(dbProvider))); - -class PartnerRepository extends DatabaseRepository { - const PartnerRepository(super.db); - - Future> getSharedBy() async { - return (await db.users.filter().isPartnerSharedByEqualTo(true).sortById().findAll()).map((u) => u.toDto()).toList(); - } - - Future> getSharedWith() async { - return (await db.users.filter().isPartnerSharedWithEqualTo(true).sortById().findAll()) - .map((u) => u.toDto()) - .toList(); - } - - Stream> watchSharedBy() { - return (db.users.filter().isPartnerSharedByEqualTo(true).sortById().watch()).map( - (users) => users.map((u) => u.toDto()).toList(), - ); - } - - Stream> watchSharedWith() { - return (db.users.filter().isPartnerSharedWithEqualTo(true).sortById().watch()).map( - (users) => users.map((u) => u.toDto()).toList(), - ); - } -} diff --git a/mobile/lib/repositories/partner_api.repository.dart b/mobile/lib/repositories/partner_api.repository.dart index d497da4d4c..69b6740cbe 100644 --- a/mobile/lib/repositories/partner_api.repository.dart +++ b/mobile/lib/repositories/partner_api.repository.dart @@ -21,8 +21,8 @@ class PartnerApiRepository extends ApiRepository { return response.map(UserConverter.fromPartnerDto).toList(); } - Future create(String id) async { - final dto = await checkNull(_api.createPartnerDeprecated(id)); + Future create(String sharedWithId) async { + final dto = await checkNull(_api.createPartner(PartnerCreateDto(sharedWithId: sharedWithId))); return UserConverter.fromPartnerDto(dto); } diff --git a/mobile/lib/repositories/timeline.repository.dart b/mobile/lib/repositories/timeline.repository.dart deleted file mode 100644 index c8c173b6f6..0000000000 --- a/mobile/lib/repositories/timeline.repository.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:isar/isar.dart'; - -final timelineRepositoryProvider = Provider((ref) => TimelineRepository(ref.watch(dbProvider))); - -class TimelineRepository extends DatabaseRepository { - const TimelineRepository(super.db); - - Future> getTimelineUserIds(String id) { - return db.users.filter().inTimelineEqualTo(true).or().idEqualTo(id).idProperty().findAll(); - } - - Stream> watchTimelineUsers(String id) { - return db.users.filter().inTimelineEqualTo(true).or().idEqualTo(id).idProperty().watch(); - } - - Stream watchArchiveTimeline(String userId) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isTrashedEqualTo(false) - .visibilityEqualTo(AssetVisibilityEnum.archive) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchFavoriteTimeline(String userId) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isFavoriteEqualTo(true) - .not() - .visibilityEqualTo(AssetVisibilityEnum.locked) - .isTrashedEqualTo(false) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchAlbumTimeline(Album album, GroupAssetsBy groupAssetByOption) { - final query = album.assets.filter().isTrashedEqualTo(false).not().visibilityEqualTo(AssetVisibilityEnum.locked); - - final withSortedOption = switch (album.sortOrder) { - SortOrder.asc => query.sortByFileCreatedAt(), - SortOrder.desc => query.sortByFileCreatedAtDesc(), - }; - - return _watchRenderList(withSortedOption, groupAssetByOption); - } - - Stream watchTrashTimeline(String userId) { - final query = db.assets.filter().ownerIdEqualTo(fastHash(userId)).isTrashedEqualTo(true).sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchAllVideosTimeline(String userId) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isTrashedEqualTo(false) - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .typeEqualTo(AssetType.video) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchHomeTimeline(String userId, GroupAssetsBy groupAssetByOption) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isTrashedEqualTo(false) - .stackPrimaryAssetIdIsNull() - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, groupAssetByOption); - } - - Stream watchMultiUsersTimeline(List userIds, GroupAssetsBy groupAssetByOption) { - final isarUserIds = userIds.map(fastHash).toList(); - final query = db.assets - .where() - .anyOf(isarUserIds, (qb, id) => qb.ownerIdEqualToAnyChecksum(id)) - .filter() - .isTrashedEqualTo(false) - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .stackPrimaryAssetIdIsNull() - .sortByFileCreatedAtDesc(); - return _watchRenderList(query, groupAssetByOption); - } - - Future getTimelineFromAssets(List assets, GroupAssetsBy getGroupByOption) { - return RenderList.fromAssets(assets, getGroupByOption); - } - - Stream watchAssetSelectionTimeline(String userId) { - final query = db.assets - .where() - .remoteIdIsNotNull() - .filter() - .ownerIdEqualTo(fastHash(userId)) - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .isTrashedEqualTo(false) - .stackPrimaryAssetIdIsNull() - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchLockedTimeline(String userId, GroupAssetsBy getGroupByOption) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .visibilityEqualTo(AssetVisibilityEnum.locked) - .isTrashedEqualTo(false) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, getGroupByOption); - } - - Stream _watchRenderList( - QueryBuilder query, - GroupAssetsBy groupAssetsBy, - ) async* { - yield await RenderList.fromQuery(query, groupAssetsBy); - await for (final _ in query.watchLazy()) { - yield await RenderList.fromQuery(query, groupAssetsBy); - } - } -} diff --git a/mobile/lib/routing/app_navigation_observer.dart b/mobile/lib/routing/app_navigation_observer.dart index af32fe2370..a68da899b1 100644 --- a/mobile/lib/routing/app_navigation_observer.dart +++ b/mobile/lib/routing/app_navigation_observer.dart @@ -19,7 +19,6 @@ class AppNavigationObserver extends AutoRouterObserver { @override void didPush(Route route, Route? previousRoute) { - _handleLockedViewState(route, previousRoute); _handleDriftLockedFolderState(route, previousRoute); Future(() { ref.read(currentRouteNameProvider.notifier).state = route.settings.name; @@ -30,7 +29,6 @@ class AppNavigationObserver extends AutoRouterObserver { @override void didPop(Route route, Route? previousRoute) { - _handleLockedViewState(previousRoute ?? route, null); _handleDriftLockedFolderState(previousRoute ?? route, null); Future(() { ref.read(currentRouteNameProvider.notifier).state = previousRoute?.settings.name; @@ -39,21 +37,6 @@ class AppNavigationObserver extends AutoRouterObserver { }); } - _handleLockedViewState(Route route, Route? previousRoute) { - final isInLockedView = ref.read(inLockedViewProvider); - final isFromLockedViewToDetailView = - route.settings.name == GalleryViewerRoute.name && previousRoute?.settings.name == LockedRoute.name; - - final isFromDetailViewToInfoPanelView = - route.settings.name == null && previousRoute?.settings.name == GalleryViewerRoute.name && isInLockedView; - - if (route.settings.name == LockedRoute.name || isFromLockedViewToDetailView || isFromDetailViewToInfoPanelView) { - Future(() => ref.read(inLockedViewProvider.notifier).state = true); - } else { - Future(() => ref.read(inLockedViewProvider.notifier).state = false); - } - } - _handleDriftLockedFolderState(Route route, Route? previousRoute) { final isInLockedView = ref.read(inLockedViewProvider); final isFromLockedViewToDetailView = diff --git a/mobile/lib/routing/backup_permission_guard.dart b/mobile/lib/routing/backup_permission_guard.dart deleted file mode 100644 index f52516f2e5..0000000000 --- a/mobile/lib/routing/backup_permission_guard.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -class BackupPermissionGuard extends AutoRouteGuard { - final GalleryPermissionNotifier _permission; - - const BackupPermissionGuard(this._permission); - - @override - void onNavigation(NavigationResolver resolver, StackRouter router) async { - final p = _permission.hasPermission; - if (p) { - resolver.next(true); - } else { - unawaited(router.push(const PermissionOnboardingRoute())); - } - } -} diff --git a/mobile/lib/routing/gallery_guard.dart b/mobile/lib/routing/gallery_guard.dart deleted file mode 100644 index 6a4b1bddab..0000000000 --- a/mobile/lib/routing/gallery_guard.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:immich_mobile/routing/router.dart'; - -/// Handles duplicate navigation to this route (primarily for deep linking) -class GalleryGuard extends AutoRouteGuard { - const GalleryGuard(); - @override - void onNavigation(NavigationResolver resolver, StackRouter router) async { - final newRouteName = resolver.route.name; - final currentTopRouteName = router.stack.isNotEmpty ? router.stack.last.name : null; - - if (currentTopRouteName == newRouteName) { - // Replace instead of pushing duplicate - final args = resolver.route.args as GalleryViewerRouteArgs; - - unawaited( - router.replace( - GalleryViewerRoute( - renderList: args.renderList, - initialIndex: args.initialIndex, - heroOffset: args.heroOffset, - showStack: args.showStack, - ), - ), - ); - // Prevent further navigation since we replaced the route - resolver.next(false); - return; - } - resolver.next(true); - } -} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index b385bcbf71..76c9d2efd2 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -1,81 +1,38 @@ import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.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/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/models/shared_link/shared_link.model.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; -import 'package:immich_mobile/pages/album/album_additional_shared_user_selection.page.dart'; -import 'package:immich_mobile/pages/album/album_asset_selection.page.dart'; -import 'package:immich_mobile/pages/album/album_options.page.dart'; -import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart'; -import 'package:immich_mobile/pages/album/album_viewer.page.dart'; -import 'package:immich_mobile/pages/albums/albums.page.dart'; -import 'package:immich_mobile/pages/backup/album_preview.page.dart'; -import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart'; -import 'package:immich_mobile/pages/backup/backup_controller.page.dart'; -import 'package:immich_mobile/pages/backup/backup_options.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_asset_detail.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_options.page.dart'; import 'package:immich_mobile/pages/backup/drift_upload_detail.page.dart'; -import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart'; -import 'package:immich_mobile/pages/common/activities.page.dart'; import 'package:immich_mobile/pages/common/app_log.page.dart'; import 'package:immich_mobile/pages/common/app_log_detail.page.dart'; -import 'package:immich_mobile/pages/common/change_experience.page.dart'; -import 'package:immich_mobile/pages/common/create_album.page.dart'; -import 'package:immich_mobile/pages/common/gallery_viewer.page.dart'; import 'package:immich_mobile/pages/common/headers_settings.page.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/pages/common/splash_screen.page.dart'; -import 'package:immich_mobile/pages/common/tab_controller.page.dart'; import 'package:immich_mobile/pages/common/tab_shell.page.dart'; -import 'package:immich_mobile/pages/editing/crop.page.dart'; -import 'package:immich_mobile/pages/editing/edit.page.dart'; -import 'package:immich_mobile/pages/editing/filter.page.dart'; -import 'package:immich_mobile/pages/library/archive.page.dart'; -import 'package:immich_mobile/pages/library/favorite.page.dart'; import 'package:immich_mobile/pages/library/folder/folder.page.dart'; -import 'package:immich_mobile/pages/library/library.page.dart'; -import 'package:immich_mobile/pages/library/local_albums.page.dart'; -import 'package:immich_mobile/pages/library/locked/locked.page.dart'; import 'package:immich_mobile/pages/library/locked/pin_auth.page.dart'; import 'package:immich_mobile/pages/library/partner/drift_partner.page.dart'; -import 'package:immich_mobile/pages/library/partner/partner.page.dart'; -import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart'; -import 'package:immich_mobile/pages/library/people/people_collection.page.dart'; -import 'package:immich_mobile/pages/library/places/places_collection.page.dart'; import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart'; import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart'; -import 'package:immich_mobile/pages/library/trash.page.dart'; import 'package:immich_mobile/pages/login/change_password.page.dart'; import 'package:immich_mobile/pages/login/login.page.dart'; -import 'package:immich_mobile/pages/onboarding/permission_onboarding.page.dart'; -import 'package:immich_mobile/pages/photos/memory.page.dart'; -import 'package:immich_mobile/pages/photos/photos.page.dart'; -import 'package:immich_mobile/pages/search/all_motion_videos.page.dart'; -import 'package:immich_mobile/pages/search/all_people.page.dart'; -import 'package:immich_mobile/pages/search/all_places.page.dart'; -import 'package:immich_mobile/pages/search/all_videos.page.dart'; -import 'package:immich_mobile/pages/search/map/map.page.dart'; 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/sync_status.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/presentation/pages/cleanup_preview.page.dart'; @@ -105,25 +62,19 @@ import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_trash.page.dart'; import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; import 'package:immich_mobile/presentation/pages/drift_video.page.dart'; -import 'package:immich_mobile/presentation/pages/editing/drift_crop.page.dart'; -import 'package:immich_mobile/presentation/pages/profile/profile_picture_crop.page.dart'; -import 'package:immich_mobile/presentation/pages/editing/drift_edit.page.dart'; -import 'package:immich_mobile/presentation/pages/editing/drift_filter.page.dart'; +import 'package:immich_mobile/presentation/pages/edit/drift_edit.page.dart'; import 'package:immich_mobile/presentation/pages/local_timeline.page.dart'; +import 'package:immich_mobile/presentation/pages/profile/profile_picture_crop.page.dart'; import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; -import 'package:immich_mobile/routing/backup_permission_guard.dart'; -import 'package:immich_mobile/routing/custom_transition_builders.dart'; import 'package:immich_mobile/routing/duplicate_guard.dart'; -import 'package:immich_mobile/routing/gallery_guard.dart'; import 'package:immich_mobile/routing/locked_guard.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/local_auth.service.dart'; import 'package:immich_mobile/services/secure_storage.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; part 'router.gr.dart'; @@ -141,9 +92,7 @@ final appRouterProvider = Provider( class AppRouter extends RootStackRouter { late final AuthGuard _authGuard; late final DuplicateGuard _duplicateGuard; - late final BackupPermissionGuard _backupPermissionGuard; late final LockedGuard _lockedGuard; - late final GalleryGuard _galleryGuard; AppRouter( ApiService apiService, @@ -154,8 +103,6 @@ class AppRouter extends RootStackRouter { _authGuard = AuthGuard(apiService); _duplicateGuard = const DuplicateGuard(); _lockedGuard = LockedGuard(apiService, secureStorageService, localAuthService); - _backupPermissionGuard = BackupPermissionGuard(galleryPermissionNotifier); - _galleryGuard = const GalleryGuard(); } @override @@ -164,20 +111,8 @@ class AppRouter extends RootStackRouter { @override late final List routes = [ AutoRoute(page: SplashScreenRoute.page, initial: true), - AutoRoute(page: PermissionOnboardingRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LoginRoute.page), AutoRoute(page: ChangePasswordRoute.page), - AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), - AutoRoute( - page: TabControllerRoute.page, - guards: [_authGuard, _duplicateGuard], - children: [ - AutoRoute(page: PhotosRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), - AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), - ], - ), AutoRoute( page: TabShellRoute.page, guards: [_authGuard, _duplicateGuard], @@ -188,105 +123,17 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), ], ), - CustomRoute( - page: GalleryViewerRoute.page, - guards: [_authGuard, _galleryGuard], - transitionsBuilder: CustomTransitionsBuilders.zoomedPage, - ), - AutoRoute(page: BackupControllerRoute.page, guards: [_authGuard, _duplicateGuard, _backupPermissionGuard]), - AutoRoute(page: AllPlacesRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: CreateAlbumRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: EditImageRoute.page), - AutoRoute(page: CropImageRoute.page), - AutoRoute(page: FilterImageRoute.page), AutoRoute(page: ProfilePictureCropRoute.page), - CustomRoute( - page: FavoritesRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AllMotionPhotosRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: RecentlyTakenRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: AlbumAssetSelectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), - CustomRoute( - page: AlbumSharedUserSelectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), - AutoRoute(page: AlbumViewerRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: AlbumAdditionalSharedUserSelectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), - AutoRoute(page: BackupAlbumSelectionRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AlbumPreviewRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: FailedBackupStatusRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), AutoRoute(page: SettingsRoute.page, guards: [_duplicateGuard]), AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]), - CustomRoute( - page: ArchiveRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: PartnerRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), AutoRoute(page: FolderRoute.page, guards: [_authGuard]), - AutoRoute(page: PartnerDetailRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: PersonResultRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AllPeopleRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: MemoryRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: MapRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AlbumOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: TrashRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SharedLinkRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SharedLinkEditRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: ActivitiesRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - durationInMilliseconds: 200, - ), CustomRoute(page: MapLocationPickerRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: BackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: HeaderSettingsRoute.page, guards: [_duplicateGuard]), - CustomRoute( - page: PeopleCollectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: AlbumsRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: LocalAlbumsRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: PlacesCollectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - AutoRoute(page: NativeVideoViewerRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: ShareIntentRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: LockedRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]), AutoRoute(page: PinAuthRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LocalMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: RemoteMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]), @@ -323,7 +170,6 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftPlaceRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftPlaceDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftUserSelectionRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: ChangeExperienceRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftPartnerRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftUploadDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SyncStatusRoute.page, guards: [_duplicateGuard]), @@ -333,8 +179,6 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftAlbumOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftMapRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftEditImageRoute.page), - AutoRoute(page: DriftCropImageRoute.page), - 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]), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 2d57c16573..c025da0f73 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -10,330 +10,6 @@ part of 'router.dart'; -/// generated route for -/// [ActivitiesPage] -class ActivitiesRoute extends PageRouteInfo { - const ActivitiesRoute({List? children}) - : super(ActivitiesRoute.name, initialChildren: children); - - static const String name = 'ActivitiesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const ActivitiesPage(); - }, - ); -} - -/// generated route for -/// [AlbumAdditionalSharedUserSelectionPage] -class AlbumAdditionalSharedUserSelectionRoute - extends PageRouteInfo { - AlbumAdditionalSharedUserSelectionRoute({ - Key? key, - required Album album, - List? children, - }) : super( - AlbumAdditionalSharedUserSelectionRoute.name, - args: AlbumAdditionalSharedUserSelectionRouteArgs( - key: key, - album: album, - ), - initialChildren: children, - ); - - static const String name = 'AlbumAdditionalSharedUserSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumAdditionalSharedUserSelectionPage( - key: args.key, - album: args.album, - ); - }, - ); -} - -class AlbumAdditionalSharedUserSelectionRouteArgs { - const AlbumAdditionalSharedUserSelectionRouteArgs({ - this.key, - required this.album, - }); - - final Key? key; - - final Album album; - - @override - String toString() { - return 'AlbumAdditionalSharedUserSelectionRouteArgs{key: $key, album: $album}'; - } -} - -/// generated route for -/// [AlbumAssetSelectionPage] -class AlbumAssetSelectionRoute - extends PageRouteInfo { - AlbumAssetSelectionRoute({ - Key? key, - required Set existingAssets, - bool canDeselect = false, - List? children, - }) : super( - AlbumAssetSelectionRoute.name, - args: AlbumAssetSelectionRouteArgs( - key: key, - existingAssets: existingAssets, - canDeselect: canDeselect, - ), - initialChildren: children, - ); - - static const String name = 'AlbumAssetSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumAssetSelectionPage( - key: args.key, - existingAssets: args.existingAssets, - canDeselect: args.canDeselect, - ); - }, - ); -} - -class AlbumAssetSelectionRouteArgs { - const AlbumAssetSelectionRouteArgs({ - this.key, - required this.existingAssets, - this.canDeselect = false, - }); - - final Key? key; - - final Set existingAssets; - - final bool canDeselect; - - @override - String toString() { - return 'AlbumAssetSelectionRouteArgs{key: $key, existingAssets: $existingAssets, canDeselect: $canDeselect}'; - } -} - -/// generated route for -/// [AlbumOptionsPage] -class AlbumOptionsRoute extends PageRouteInfo { - const AlbumOptionsRoute({List? children}) - : super(AlbumOptionsRoute.name, initialChildren: children); - - static const String name = 'AlbumOptionsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AlbumOptionsPage(); - }, - ); -} - -/// generated route for -/// [AlbumPreviewPage] -class AlbumPreviewRoute extends PageRouteInfo { - AlbumPreviewRoute({ - Key? key, - required Album album, - List? children, - }) : super( - AlbumPreviewRoute.name, - args: AlbumPreviewRouteArgs(key: key, album: album), - initialChildren: children, - ); - - static const String name = 'AlbumPreviewRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumPreviewPage(key: args.key, album: args.album); - }, - ); -} - -class AlbumPreviewRouteArgs { - const AlbumPreviewRouteArgs({this.key, required this.album}); - - final Key? key; - - final Album album; - - @override - String toString() { - return 'AlbumPreviewRouteArgs{key: $key, album: $album}'; - } -} - -/// generated route for -/// [AlbumSharedUserSelectionPage] -class AlbumSharedUserSelectionRoute - extends PageRouteInfo { - AlbumSharedUserSelectionRoute({ - Key? key, - required Set assets, - List? children, - }) : super( - AlbumSharedUserSelectionRoute.name, - args: AlbumSharedUserSelectionRouteArgs(key: key, assets: assets), - initialChildren: children, - ); - - static const String name = 'AlbumSharedUserSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumSharedUserSelectionPage(key: args.key, assets: args.assets); - }, - ); -} - -class AlbumSharedUserSelectionRouteArgs { - const AlbumSharedUserSelectionRouteArgs({this.key, required this.assets}); - - final Key? key; - - final Set assets; - - @override - String toString() { - return 'AlbumSharedUserSelectionRouteArgs{key: $key, assets: $assets}'; - } -} - -/// generated route for -/// [AlbumViewerPage] -class AlbumViewerRoute extends PageRouteInfo { - AlbumViewerRoute({ - Key? key, - required int albumId, - List? children, - }) : super( - AlbumViewerRoute.name, - args: AlbumViewerRouteArgs(key: key, albumId: albumId), - initialChildren: children, - ); - - static const String name = 'AlbumViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumViewerPage(key: args.key, albumId: args.albumId); - }, - ); -} - -class AlbumViewerRouteArgs { - const AlbumViewerRouteArgs({this.key, required this.albumId}); - - final Key? key; - - final int albumId; - - @override - String toString() { - return 'AlbumViewerRouteArgs{key: $key, albumId: $albumId}'; - } -} - -/// generated route for -/// [AlbumsPage] -class AlbumsRoute extends PageRouteInfo { - const AlbumsRoute({List? children}) - : super(AlbumsRoute.name, initialChildren: children); - - static const String name = 'AlbumsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AlbumsPage(); - }, - ); -} - -/// generated route for -/// [AllMotionPhotosPage] -class AllMotionPhotosRoute extends PageRouteInfo { - const AllMotionPhotosRoute({List? children}) - : super(AllMotionPhotosRoute.name, initialChildren: children); - - static const String name = 'AllMotionPhotosRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllMotionPhotosPage(); - }, - ); -} - -/// generated route for -/// [AllPeoplePage] -class AllPeopleRoute extends PageRouteInfo { - const AllPeopleRoute({List? children}) - : super(AllPeopleRoute.name, initialChildren: children); - - static const String name = 'AllPeopleRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllPeoplePage(); - }, - ); -} - -/// generated route for -/// [AllPlacesPage] -class AllPlacesRoute extends PageRouteInfo { - const AllPlacesRoute({List? children}) - : super(AllPlacesRoute.name, initialChildren: children); - - static const String name = 'AllPlacesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllPlacesPage(); - }, - ); -} - -/// generated route for -/// [AllVideosPage] -class AllVideosRoute extends PageRouteInfo { - const AllVideosRoute({List? children}) - : super(AllVideosRoute.name, initialChildren: children); - - static const String name = 'AllVideosRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllVideosPage(); - }, - ); -} - /// generated route for /// [AppLogDetailPage] class AppLogDetailRoute extends PageRouteInfo { @@ -369,6 +45,16 @@ class AppLogDetailRouteArgs { String toString() { return 'AppLogDetailRouteArgs{key: $key, logMessage: $logMessage}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! AppLogDetailRouteArgs) return false; + return key == other.key && logMessage == other.logMessage; + } + + @override + int get hashCode => key.hashCode ^ logMessage.hashCode; } /// generated route for @@ -387,22 +73,6 @@ class AppLogRoute extends PageRouteInfo { ); } -/// generated route for -/// [ArchivePage] -class ArchiveRoute extends PageRouteInfo { - const ArchiveRoute({List? children}) - : super(ArchiveRoute.name, initialChildren: children); - - static const String name = 'ArchiveRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const ArchivePage(); - }, - ); -} - /// generated route for /// [AssetTroubleshootPage] class AssetTroubleshootRoute extends PageRouteInfo { @@ -438,6 +108,16 @@ class AssetTroubleshootRouteArgs { String toString() { return 'AssetTroubleshootRouteArgs{key: $key, asset: $asset}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! AssetTroubleshootRouteArgs) return false; + return key == other.key && asset == other.asset; + } + + @override + int get hashCode => key.hashCode ^ asset.hashCode; } /// generated route for @@ -502,97 +182,25 @@ class AssetViewerRouteArgs { String toString() { return 'AssetViewerRouteArgs{key: $key, initialIndex: $initialIndex, timelineService: $timelineService, heroOffset: $heroOffset, currentAlbum: $currentAlbum}'; } -} - -/// generated route for -/// [BackupAlbumSelectionPage] -class BackupAlbumSelectionRoute extends PageRouteInfo { - const BackupAlbumSelectionRoute({List? children}) - : super(BackupAlbumSelectionRoute.name, initialChildren: children); - - static const String name = 'BackupAlbumSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BackupAlbumSelectionPage(); - }, - ); -} - -/// generated route for -/// [BackupControllerPage] -class BackupControllerRoute extends PageRouteInfo { - const BackupControllerRoute({List? children}) - : super(BackupControllerRoute.name, initialChildren: children); - - static const String name = 'BackupControllerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BackupControllerPage(); - }, - ); -} - -/// generated route for -/// [BackupOptionsPage] -class BackupOptionsRoute extends PageRouteInfo { - const BackupOptionsRoute({List? children}) - : super(BackupOptionsRoute.name, initialChildren: children); - - static const String name = 'BackupOptionsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BackupOptionsPage(); - }, - ); -} - -/// generated route for -/// [ChangeExperiencePage] -class ChangeExperienceRoute extends PageRouteInfo { - ChangeExperienceRoute({ - Key? key, - required bool switchingToBeta, - List? children, - }) : super( - ChangeExperienceRoute.name, - args: ChangeExperienceRouteArgs( - key: key, - switchingToBeta: switchingToBeta, - ), - initialChildren: children, - ); - - static const String name = 'ChangeExperienceRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return ChangeExperiencePage( - key: args.key, - switchingToBeta: args.switchingToBeta, - ); - }, - ); -} - -class ChangeExperienceRouteArgs { - const ChangeExperienceRouteArgs({this.key, required this.switchingToBeta}); - - final Key? key; - - final bool switchingToBeta; @override - String toString() { - return 'ChangeExperienceRouteArgs{key: $key, switchingToBeta: $switchingToBeta}'; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! AssetViewerRouteArgs) return false; + return key == other.key && + initialIndex == other.initialIndex && + timelineService == other.timelineService && + heroOffset == other.heroOffset && + currentAlbum == other.currentAlbum; } + + @override + int get hashCode => + key.hashCode ^ + initialIndex.hashCode ^ + timelineService.hashCode ^ + heroOffset.hashCode ^ + currentAlbum.hashCode; } /// generated route for @@ -646,89 +254,18 @@ class CleanupPreviewRouteArgs { String toString() { return 'CleanupPreviewRouteArgs{key: $key, assets: $assets}'; } -} - -/// generated route for -/// [CreateAlbumPage] -class CreateAlbumRoute extends PageRouteInfo { - CreateAlbumRoute({ - Key? key, - List? assets, - List? children, - }) : super( - CreateAlbumRoute.name, - args: CreateAlbumRouteArgs(key: key, assets: assets), - initialChildren: children, - ); - - static const String name = 'CreateAlbumRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const CreateAlbumRouteArgs(), - ); - return CreateAlbumPage(key: args.key, assets: args.assets); - }, - ); -} - -class CreateAlbumRouteArgs { - const CreateAlbumRouteArgs({this.key, this.assets}); - - final Key? key; - - final List? assets; @override - String toString() { - return 'CreateAlbumRouteArgs{key: $key, assets: $assets}'; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! CleanupPreviewRouteArgs) return false; + return key == other.key && + const ListEquality().equals(assets, other.assets); } -} - -/// generated route for -/// [CropImagePage] -class CropImageRoute extends PageRouteInfo { - CropImageRoute({ - Key? key, - required Image image, - required Asset asset, - List? children, - }) : super( - CropImageRoute.name, - args: CropImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'CropImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return CropImagePage(key: args.key, image: args.image, asset: args.asset); - }, - ); -} - -class CropImageRouteArgs { - const CropImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final Asset asset; @override - String toString() { - return 'CropImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } + int get hashCode => + key.hashCode ^ const ListEquality().hash(assets); } /// generated route for @@ -803,6 +340,20 @@ class DriftActivitiesRouteArgs { String toString() { return 'DriftActivitiesRouteArgs{key: $key, album: $album, assetId: $assetId, assetName: $assetName}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! DriftActivitiesRouteArgs) return false; + return key == other.key && + album == other.album && + assetId == other.assetId && + assetName == other.assetName; + } + + @override + int get hashCode => + key.hashCode ^ album.hashCode ^ assetId.hashCode ^ assetName.hashCode; } /// generated route for @@ -840,6 +391,16 @@ class DriftAlbumOptionsRouteArgs { String toString() { return 'DriftAlbumOptionsRouteArgs{key: $key, album: $album}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! DriftAlbumOptionsRouteArgs) return false; + return key == other.key && album == other.album; + } + + @override + int get hashCode => key.hashCode ^ album.hashCode; } /// generated route for @@ -921,6 +482,21 @@ class DriftAssetSelectionTimelineRouteArgs { String toString() { return 'DriftAssetSelectionTimelineRouteArgs{key: $key, lockedSelectionAssets: $lockedSelectionAssets}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! DriftAssetSelectionTimelineRouteArgs) return false; + return key == other.key && + const SetEquality().equals( + lockedSelectionAssets, + other.lockedSelectionAssets, + ); + } + + @override + int get hashCode => + key.hashCode ^ const SetEquality().hash(lockedSelectionAssets); } /// generated route for @@ -1003,70 +579,20 @@ class DriftCreateAlbumRoute extends PageRouteInfo { ); } -/// generated route for -/// [DriftCropImagePage] -class DriftCropImageRoute extends PageRouteInfo { - DriftCropImageRoute({ - Key? key, - required Image image, - required BaseAsset asset, - List? children, - }) : super( - DriftCropImageRoute.name, - args: DriftCropImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'DriftCropImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftCropImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); - }, - ); -} - -class DriftCropImageRouteArgs { - const DriftCropImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final BaseAsset asset; - - @override - String toString() { - return 'DriftCropImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [DriftEditImagePage] class DriftEditImageRoute extends PageRouteInfo { DriftEditImageRoute({ Key? key, - required BaseAsset asset, required Image image, - required bool isEdited, + required Future Function(List) applyEdits, List? children, }) : super( DriftEditImageRoute.name, args: DriftEditImageRouteArgs( key: key, - asset: asset, image: image, - isEdited: isEdited, + applyEdits: applyEdits, ), initialChildren: children, ); @@ -1079,9 +605,8 @@ class DriftEditImageRoute extends PageRouteInfo { final args = data.argsAs(); return DriftEditImagePage( key: args.key, - asset: args.asset, image: args.image, - isEdited: args.isEdited, + applyEdits: args.applyEdits, ); }, ); @@ -1090,23 +615,30 @@ class DriftEditImageRoute extends PageRouteInfo { class DriftEditImageRouteArgs { const DriftEditImageRouteArgs({ this.key, - required this.asset, required this.image, - required this.isEdited, + required this.applyEdits, }); final Key? key; - final BaseAsset asset; - final Image image; - final bool isEdited; + final Future Function(List) applyEdits; @override String toString() { - return 'DriftEditImageRouteArgs{key: $key, asset: $asset, image: $image, isEdited: $isEdited}'; + return 'DriftEditImageRouteArgs{key: $key, image: $image, applyEdits: $applyEdits}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! DriftEditImageRouteArgs) return false; + return key == other.key && image == other.image; + } + + @override + int get hashCode => key.hashCode ^ image.hashCode; } /// generated route for @@ -1125,54 +657,6 @@ class DriftFavoriteRoute extends PageRouteInfo { ); } -/// generated route for -/// [DriftFilterImagePage] -class DriftFilterImageRoute extends PageRouteInfo { - DriftFilterImageRoute({ - Key? key, - required Image image, - required BaseAsset asset, - List? children, - }) : super( - DriftFilterImageRoute.name, - args: DriftFilterImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'DriftFilterImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftFilterImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); - }, - ); -} - -class DriftFilterImageRouteArgs { - const DriftFilterImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final BaseAsset asset; - - @override - String toString() { - return 'DriftFilterImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [DriftLibraryPage] class DriftLibraryRoute extends PageRouteInfo { @@ -1258,6 +742,16 @@ class DriftMapRouteArgs { String toString() { return 'DriftMapRouteArgs{key: $key, initialLocation: $initialLocation}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! DriftMapRouteArgs) return false; + return key == other.key && initialLocation == other.initialLocation; + } + + @override + int get hashCode => key.hashCode ^ initialLocation.hashCode; } /// generated route for @@ -1310,6 +804,21 @@ class DriftMemoryRouteArgs { String toString() { return 'DriftMemoryRouteArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! DriftMemoryRouteArgs) return false; + return const ListEquality().equals(memories, other.memories) && + memoryIndex == other.memoryIndex && + key == other.key; + } + + @override + int get hashCode => + const ListEquality().hash(memories) ^ + memoryIndex.hashCode ^ + key.hashCode; } /// generated route for @@ -1348,6 +857,16 @@ class DriftPartnerDetailRouteArgs { String toString() { return 'DriftPartnerDetailRouteArgs{key: $key, partner: $partner}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! DriftPartnerDetailRouteArgs) return false; + return key == other.key && partner == other.partner; + } + + @override + int get hashCode => key.hashCode ^ partner.hashCode; } /// generated route for @@ -1417,6 +936,16 @@ class DriftPersonRouteArgs { String toString() { return 'DriftPersonRouteArgs{key: $key, person: $person}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! DriftPersonRouteArgs) return false; + return key == other.key && person == other.person; + } + + @override + int get hashCode => key.hashCode ^ person.hashCode; } /// generated route for @@ -1454,6 +983,16 @@ class DriftPlaceDetailRouteArgs { String toString() { return 'DriftPlaceDetailRouteArgs{key: $key, place: $place}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! DriftPlaceDetailRouteArgs) return false; + return key == other.key && place == other.place; + } + + @override + int get hashCode => key.hashCode ^ place.hashCode; } /// generated route for @@ -1496,6 +1035,16 @@ class DriftPlaceRouteArgs { String toString() { return 'DriftPlaceRouteArgs{key: $key, currentLocation: $currentLocation}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! DriftPlaceRouteArgs) return false; + return key == other.key && currentLocation == other.currentLocation; + } + + @override + int get hashCode => key.hashCode ^ currentLocation.hashCode; } /// generated route for @@ -1598,6 +1147,16 @@ class DriftUserSelectionRouteArgs { String toString() { return 'DriftUserSelectionRouteArgs{key: $key, album: $album}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! DriftUserSelectionRouteArgs) return false; + return key == other.key && album == other.album; + } + + @override + int get hashCode => key.hashCode ^ album.hashCode; } /// generated route for @@ -1616,144 +1175,6 @@ class DriftVideoRoute extends PageRouteInfo { ); } -/// generated route for -/// [EditImagePage] -class EditImageRoute extends PageRouteInfo { - EditImageRoute({ - Key? key, - required Asset asset, - required Image image, - required bool isEdited, - List? children, - }) : super( - EditImageRoute.name, - args: EditImageRouteArgs( - key: key, - asset: asset, - image: image, - isEdited: isEdited, - ), - initialChildren: children, - ); - - static const String name = 'EditImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return EditImagePage( - key: args.key, - asset: args.asset, - image: args.image, - isEdited: args.isEdited, - ); - }, - ); -} - -class EditImageRouteArgs { - const EditImageRouteArgs({ - this.key, - required this.asset, - required this.image, - required this.isEdited, - }); - - final Key? key; - - final Asset asset; - - final Image image; - - final bool isEdited; - - @override - String toString() { - return 'EditImageRouteArgs{key: $key, asset: $asset, image: $image, isEdited: $isEdited}'; - } -} - -/// generated route for -/// [FailedBackupStatusPage] -class FailedBackupStatusRoute extends PageRouteInfo { - const FailedBackupStatusRoute({List? children}) - : super(FailedBackupStatusRoute.name, initialChildren: children); - - static const String name = 'FailedBackupStatusRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const FailedBackupStatusPage(); - }, - ); -} - -/// generated route for -/// [FavoritesPage] -class FavoritesRoute extends PageRouteInfo { - const FavoritesRoute({List? children}) - : super(FavoritesRoute.name, initialChildren: children); - - static const String name = 'FavoritesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const FavoritesPage(); - }, - ); -} - -/// generated route for -/// [FilterImagePage] -class FilterImageRoute extends PageRouteInfo { - FilterImageRoute({ - Key? key, - required Image image, - required Asset asset, - List? children, - }) : super( - FilterImageRoute.name, - args: FilterImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'FilterImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return FilterImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); - }, - ); -} - -class FilterImageRouteArgs { - const FilterImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final Asset asset; - - @override - String toString() { - return 'FilterImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [FolderPage] class FolderRoute extends PageRouteInfo { @@ -1791,70 +1212,16 @@ class FolderRouteArgs { String toString() { return 'FolderRouteArgs{key: $key, folder: $folder}'; } -} - -/// generated route for -/// [GalleryViewerPage] -class GalleryViewerRoute extends PageRouteInfo { - GalleryViewerRoute({ - Key? key, - required RenderList renderList, - int initialIndex = 0, - int heroOffset = 0, - bool showStack = false, - List? children, - }) : super( - GalleryViewerRoute.name, - args: GalleryViewerRouteArgs( - key: key, - renderList: renderList, - initialIndex: initialIndex, - heroOffset: heroOffset, - showStack: showStack, - ), - initialChildren: children, - ); - - static const String name = 'GalleryViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return GalleryViewerPage( - key: args.key, - renderList: args.renderList, - initialIndex: args.initialIndex, - heroOffset: args.heroOffset, - showStack: args.showStack, - ); - }, - ); -} - -class GalleryViewerRouteArgs { - const GalleryViewerRouteArgs({ - this.key, - required this.renderList, - this.initialIndex = 0, - this.heroOffset = 0, - this.showStack = false, - }); - - final Key? key; - - final RenderList renderList; - - final int initialIndex; - - final int heroOffset; - - final bool showStack; @override - String toString() { - return 'GalleryViewerRouteArgs{key: $key, renderList: $renderList, initialIndex: $initialIndex, heroOffset: $heroOffset, showStack: $showStack}'; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! FolderRouteArgs) return false; + return key == other.key && folder == other.folder; } + + @override + int get hashCode => key.hashCode ^ folder.hashCode; } /// generated route for @@ -1873,38 +1240,6 @@ class HeaderSettingsRoute extends PageRouteInfo { ); } -/// generated route for -/// [LibraryPage] -class LibraryRoute extends PageRouteInfo { - const LibraryRoute({List? children}) - : super(LibraryRoute.name, initialChildren: children); - - static const String name = 'LibraryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LibraryPage(); - }, - ); -} - -/// generated route for -/// [LocalAlbumsPage] -class LocalAlbumsRoute extends PageRouteInfo { - const LocalAlbumsRoute({List? children}) - : super(LocalAlbumsRoute.name, initialChildren: children); - - static const String name = 'LocalAlbumsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LocalAlbumsPage(); - }, - ); -} - /// generated route for /// [LocalMediaSummaryPage] class LocalMediaSummaryRoute extends PageRouteInfo { @@ -1956,22 +1291,16 @@ class LocalTimelineRouteArgs { String toString() { return 'LocalTimelineRouteArgs{key: $key, album: $album}'; } -} -/// generated route for -/// [LockedPage] -class LockedRoute extends PageRouteInfo { - const LockedRoute({List? children}) - : super(LockedRoute.name, initialChildren: children); + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! LocalTimelineRouteArgs) return false; + return key == other.key && album == other.album; + } - static const String name = 'LockedRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LockedPage(); - }, - ); + @override + int get hashCode => key.hashCode ^ album.hashCode; } /// generated route for @@ -2052,311 +1381,16 @@ class MapLocationPickerRouteArgs { String toString() { return 'MapLocationPickerRouteArgs{key: $key, initialLatLng: $initialLatLng}'; } -} - -/// generated route for -/// [MapPage] -class MapRoute extends PageRouteInfo { - MapRoute({Key? key, LatLng? initialLocation, List? children}) - : super( - MapRoute.name, - args: MapRouteArgs(key: key, initialLocation: initialLocation), - initialChildren: children, - ); - - static const String name = 'MapRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const MapRouteArgs(), - ); - return MapPage(key: args.key, initialLocation: args.initialLocation); - }, - ); -} - -class MapRouteArgs { - const MapRouteArgs({this.key, this.initialLocation}); - - final Key? key; - - final LatLng? initialLocation; @override - String toString() { - return 'MapRouteArgs{key: $key, initialLocation: $initialLocation}'; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! MapLocationPickerRouteArgs) return false; + return key == other.key && initialLatLng == other.initialLatLng; } -} - -/// generated route for -/// [MemoryPage] -class MemoryRoute extends PageRouteInfo { - MemoryRoute({ - required List memories, - required int memoryIndex, - Key? key, - List? children, - }) : super( - MemoryRoute.name, - args: MemoryRouteArgs( - memories: memories, - memoryIndex: memoryIndex, - key: key, - ), - initialChildren: children, - ); - - static const String name = 'MemoryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return MemoryPage( - memories: args.memories, - memoryIndex: args.memoryIndex, - key: args.key, - ); - }, - ); -} - -class MemoryRouteArgs { - const MemoryRouteArgs({ - required this.memories, - required this.memoryIndex, - this.key, - }); - - final List memories; - - final int memoryIndex; - - final Key? key; @override - String toString() { - return 'MemoryRouteArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}'; - } -} - -/// generated route for -/// [NativeVideoViewerPage] -class NativeVideoViewerRoute extends PageRouteInfo { - NativeVideoViewerRoute({ - Key? key, - required Asset asset, - required Widget image, - bool showControls = true, - int playbackDelayFactor = 1, - List? children, - }) : super( - NativeVideoViewerRoute.name, - args: NativeVideoViewerRouteArgs( - key: key, - asset: asset, - image: image, - showControls: showControls, - playbackDelayFactor: playbackDelayFactor, - ), - initialChildren: children, - ); - - static const String name = 'NativeVideoViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return NativeVideoViewerPage( - key: args.key, - asset: args.asset, - image: args.image, - showControls: args.showControls, - playbackDelayFactor: args.playbackDelayFactor, - ); - }, - ); -} - -class NativeVideoViewerRouteArgs { - const NativeVideoViewerRouteArgs({ - this.key, - required this.asset, - required this.image, - this.showControls = true, - this.playbackDelayFactor = 1, - }); - - final Key? key; - - final Asset asset; - - final Widget image; - - final bool showControls; - - final int playbackDelayFactor; - - @override - String toString() { - return 'NativeVideoViewerRouteArgs{key: $key, asset: $asset, image: $image, showControls: $showControls, playbackDelayFactor: $playbackDelayFactor}'; - } -} - -/// generated route for -/// [PartnerDetailPage] -class PartnerDetailRoute extends PageRouteInfo { - PartnerDetailRoute({ - Key? key, - required UserDto partner, - List? children, - }) : super( - PartnerDetailRoute.name, - args: PartnerDetailRouteArgs(key: key, partner: partner), - initialChildren: children, - ); - - static const String name = 'PartnerDetailRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return PartnerDetailPage(key: args.key, partner: args.partner); - }, - ); -} - -class PartnerDetailRouteArgs { - const PartnerDetailRouteArgs({this.key, required this.partner}); - - final Key? key; - - final UserDto partner; - - @override - String toString() { - return 'PartnerDetailRouteArgs{key: $key, partner: $partner}'; - } -} - -/// generated route for -/// [PartnerPage] -class PartnerRoute extends PageRouteInfo { - const PartnerRoute({List? children}) - : super(PartnerRoute.name, initialChildren: children); - - static const String name = 'PartnerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PartnerPage(); - }, - ); -} - -/// generated route for -/// [PeopleCollectionPage] -class PeopleCollectionRoute extends PageRouteInfo { - const PeopleCollectionRoute({List? children}) - : super(PeopleCollectionRoute.name, initialChildren: children); - - static const String name = 'PeopleCollectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PeopleCollectionPage(); - }, - ); -} - -/// generated route for -/// [PermissionOnboardingPage] -class PermissionOnboardingRoute extends PageRouteInfo { - const PermissionOnboardingRoute({List? children}) - : super(PermissionOnboardingRoute.name, initialChildren: children); - - static const String name = 'PermissionOnboardingRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PermissionOnboardingPage(); - }, - ); -} - -/// generated route for -/// [PersonResultPage] -class PersonResultRoute extends PageRouteInfo { - PersonResultRoute({ - Key? key, - required String personId, - required String personName, - List? children, - }) : super( - PersonResultRoute.name, - args: PersonResultRouteArgs( - key: key, - personId: personId, - personName: personName, - ), - initialChildren: children, - ); - - static const String name = 'PersonResultRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return PersonResultPage( - key: args.key, - personId: args.personId, - personName: args.personName, - ); - }, - ); -} - -class PersonResultRouteArgs { - const PersonResultRouteArgs({ - this.key, - required this.personId, - required this.personName, - }); - - final Key? key; - - final String personId; - - final String personName; - - @override - String toString() { - return 'PersonResultRouteArgs{key: $key, personId: $personId, personName: $personName}'; - } -} - -/// generated route for -/// [PhotosPage] -class PhotosRoute extends PageRouteInfo { - const PhotosRoute({List? children}) - : super(PhotosRoute.name, initialChildren: children); - - static const String name = 'PhotosRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PhotosPage(); - }, - ); + int get hashCode => key.hashCode ^ initialLatLng.hashCode; } /// generated route for @@ -2396,51 +1430,16 @@ class PinAuthRouteArgs { String toString() { return 'PinAuthRouteArgs{key: $key, createPinCode: $createPinCode}'; } -} - -/// generated route for -/// [PlacesCollectionPage] -class PlacesCollectionRoute extends PageRouteInfo { - PlacesCollectionRoute({ - Key? key, - LatLng? currentLocation, - List? children, - }) : super( - PlacesCollectionRoute.name, - args: PlacesCollectionRouteArgs( - key: key, - currentLocation: currentLocation, - ), - initialChildren: children, - ); - - static const String name = 'PlacesCollectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const PlacesCollectionRouteArgs(), - ); - return PlacesCollectionPage( - key: args.key, - currentLocation: args.currentLocation, - ); - }, - ); -} - -class PlacesCollectionRouteArgs { - const PlacesCollectionRouteArgs({this.key, this.currentLocation}); - - final Key? key; - - final LatLng? currentLocation; @override - String toString() { - return 'PlacesCollectionRouteArgs{key: $key, currentLocation: $currentLocation}'; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! PinAuthRouteArgs) return false; + return key == other.key && createPinCode == other.createPinCode; } + + @override + int get hashCode => key.hashCode ^ createPinCode.hashCode; } /// generated route for @@ -2479,22 +1478,16 @@ class ProfilePictureCropRouteArgs { String toString() { return 'ProfilePictureCropRouteArgs{key: $key, asset: $asset}'; } -} -/// generated route for -/// [RecentlyTakenPage] -class RecentlyTakenRoute extends PageRouteInfo { - const RecentlyTakenRoute({List? children}) - : super(RecentlyTakenRoute.name, initialChildren: children); + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! ProfilePictureCropRouteArgs) return false; + return key == other.key && asset == other.asset; + } - static const String name = 'RecentlyTakenRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const RecentlyTakenPage(); - }, - ); + @override + int get hashCode => key.hashCode ^ asset.hashCode; } /// generated route for @@ -2532,6 +1525,16 @@ class RemoteAlbumRouteArgs { String toString() { return 'RemoteAlbumRouteArgs{key: $key, album: $album}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! RemoteAlbumRouteArgs) return false; + return key == other.key && album == other.album; + } + + @override + int get hashCode => key.hashCode ^ album.hashCode; } /// generated route for @@ -2550,45 +1553,6 @@ class RemoteMediaSummaryRoute extends PageRouteInfo { ); } -/// generated route for -/// [SearchPage] -class SearchRoute extends PageRouteInfo { - SearchRoute({ - Key? key, - SearchFilter? prefilter, - List? children, - }) : super( - SearchRoute.name, - args: SearchRouteArgs(key: key, prefilter: prefilter), - initialChildren: children, - ); - - static const String name = 'SearchRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const SearchRouteArgs(), - ); - return SearchPage(key: args.key, prefilter: args.prefilter); - }, - ); -} - -class SearchRouteArgs { - const SearchRouteArgs({this.key, this.prefilter}); - - final Key? key; - - final SearchFilter? prefilter; - - @override - String toString() { - return 'SearchRouteArgs{key: $key, prefilter: $prefilter}'; - } -} - /// generated route for /// [SettingsPage] class SettingsRoute extends PageRouteInfo { @@ -2640,6 +1604,16 @@ class SettingsSubRouteArgs { String toString() { return 'SettingsSubRouteArgs{section: $section, key: $key}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! SettingsSubRouteArgs) return false; + return section == other.section && key == other.key; + } + + @override + int get hashCode => section.hashCode ^ key.hashCode; } /// generated route for @@ -2677,6 +1651,22 @@ class ShareIntentRouteArgs { String toString() { return 'ShareIntentRouteArgs{key: $key, attachments: $attachments}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! ShareIntentRouteArgs) return false; + return key == other.key && + const ListEquality().equals( + attachments, + other.attachments, + ); + } + + @override + int get hashCode => + key.hashCode ^ + const ListEquality().hash(attachments); } /// generated route for @@ -2737,6 +1727,23 @@ class SharedLinkEditRouteArgs { String toString() { return 'SharedLinkEditRouteArgs{key: $key, existingLink: $existingLink, assetsList: $assetsList, albumId: $albumId}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! SharedLinkEditRouteArgs) return false; + return key == other.key && + existingLink == other.existingLink && + const ListEquality().equals(assetsList, other.assetsList) && + albumId == other.albumId; + } + + @override + int get hashCode => + key.hashCode ^ + existingLink.hashCode ^ + const ListEquality().hash(assetsList) ^ + albumId.hashCode; } /// generated route for @@ -2787,22 +1794,6 @@ class SyncStatusRoute extends PageRouteInfo { ); } -/// generated route for -/// [TabControllerPage] -class TabControllerRoute extends PageRouteInfo { - const TabControllerRoute({List? children}) - : super(TabControllerRoute.name, initialChildren: children); - - static const String name = 'TabControllerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const TabControllerPage(); - }, - ); -} - /// generated route for /// [TabShellPage] class TabShellRoute extends PageRouteInfo { @@ -2818,19 +1809,3 @@ class TabShellRoute extends PageRouteInfo { }, ); } - -/// generated route for -/// [TrashPage] -class TrashRoute extends PageRouteInfo { - const TrashRoute({List? children}) - : super(TrashRoute.name, initialChildren: children); - - static const String name = 'TrashRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const TrashPage(); - }, - ); -} diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index c435bf9d79..4a195017d3 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; @@ -23,7 +24,6 @@ import 'package:immich_mobile/utils/timezone.dart'; import 'package:immich_mobile/widgets/common/date_time_picker.dart'; import 'package:immich_mobile/widgets/common/location_picker.dart'; import 'package:maplibre_gl/maplibre_gl.dart' as maplibre; -import 'package:riverpod_annotation/riverpod_annotation.dart'; final actionServiceProvider = Provider( (ref) => ActionService( @@ -246,6 +246,14 @@ class ActionService { return true; } + Future applyEdits(String remoteId, List edits) async { + if (edits.isEmpty) { + await _assetApiRepository.removeEdits(remoteId); + } else { + await _assetApiRepository.editAsset(remoteId, edits); + } + } + Future _deleteLocalAssets(List localIds) async { final deletedIds = await _assetMediaRepository.deleteAll(localIds); if (deletedIds.isEmpty) { diff --git a/mobile/lib/services/activity.service.dart b/mobile/lib/services/activity.service.dart index 382a7fe107..0d4709d0d5 100644 --- a/mobile/lib/services/activity.service.dart +++ b/mobile/lib/services/activity.service.dart @@ -9,7 +9,6 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da import 'package:immich_mobile/repositories/activity_api.repository.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:logging/logging.dart'; -import 'package:immich_mobile/entities/store.entity.dart' as immich_store; class ActivityService with ErrorLoggerMixin { final ActivityApiRepository _activityApiRepository; @@ -29,14 +28,6 @@ class ActivityService with ErrorLoggerMixin { ); } - Future getStatistics(String albumId, {String? assetId}) async { - return logError( - () => _activityApiRepository.getStats(albumId, assetId: assetId), - defaultValue: const ActivityStats(comments: 0), - errorMessage: "Failed to statistics for album $albumId", - ); - } - Future removeActivity(String id) async { return logError( () async { @@ -60,20 +51,16 @@ class ActivityService with ErrorLoggerMixin { } Future buildAssetViewerRoute(String assetId, WidgetRef ref) async { - if (immich_store.Store.isBetaTimelineEnabled) { - final asset = await _assetService.getRemoteAsset(assetId); - if (asset == null) { - return null; - } - - AssetViewer.setAsset(ref, asset); - return AssetViewerRoute( - initialIndex: 0, - timelineService: _timelineFactory.fromAssets([asset], TimelineOrigin.albumActivities), - currentAlbum: ref.read(currentRemoteAlbumProvider), - ); + final asset = await _assetService.getRemoteAsset(assetId); + if (asset == null) { + return null; } - return null; + AssetViewer.setAsset(ref, asset); + return AssetViewerRoute( + initialIndex: 0, + timelineService: _timelineFactory.fromAssets([asset], TimelineOrigin.albumActivities), + currentAlbum: ref.read(currentRemoteAlbumProvider), + ); } } diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart deleted file mode 100644 index 8d77b569e6..0000000000 --- a/mobile/lib/services/album.service.dart +++ /dev/null @@ -1,425 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart'; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/album.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -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( - ref.watch(syncServiceProvider), - ref.watch(userServiceProvider), - ref.watch(entityServiceProvider), - ref.watch(albumRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(backupAlbumRepositoryProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(albumApiRepositoryProvider), - ), -); - -class AlbumService { - final SyncService _syncService; - final UserService _userService; - final EntityService _entityService; - final AlbumRepository _albumRepository; - final AssetRepository _assetRepository; - final BackupAlbumRepository _backupAlbumRepository; - final AlbumMediaRepository _albumMediaRepository; - final AlbumApiRepository _albumApiRepository; - final Logger _log = Logger('AlbumService'); - Completer _localCompleter = Completer()..complete(false); - Completer _remoteCompleter = Completer()..complete(false); - - AlbumService( - this._syncService, - this._userService, - this._entityService, - this._albumRepository, - this._assetRepository, - this._backupAlbumRepository, - this._albumMediaRepository, - this._albumApiRepository, - ); - - /// Checks all selected device albums for changes of albums and their assets - /// Updates the local database and returns `true` if there were any changes - Future refreshDeviceAlbums() async { - if (!_localCompleter.isCompleted) { - // guard against concurrent calls - _log.info("refreshDeviceAlbums is already in progress"); - return _localCompleter.future; - } - _localCompleter = Completer(); - final Stopwatch sw = Stopwatch()..start(); - bool changes = false; - try { - final (selectedIds, excludedIds, onDevice) = await ( - _backupAlbumRepository.getIdsBySelection(BackupSelection.select).then((value) => value.toSet()), - _backupAlbumRepository.getIdsBySelection(BackupSelection.exclude).then((value) => value.toSet()), - _albumMediaRepository.getAll(), - ).wait; - _log.info("Found ${onDevice.length} device albums"); - if (selectedIds.isEmpty) { - final numLocal = await _albumRepository.count(local: true); - if (numLocal > 0) { - await _syncService.removeAllLocalAlbumsAndAssets(); - } - return false; - } - Set? excludedAssets; - if (excludedIds.isNotEmpty) { - if (Platform.isIOS) { - // iOS and Android device album working principle differ significantly - // on iOS, an asset can be in multiple albums - // on Android, an asset can only be in exactly one album (folder!) at the same time - // thus, on Android, excluding an album can be done by ignoring that album - // however, on iOS, it it necessary to load the assets from all excluded - // albums and check every asset from any selected album against the set - // of excluded assets - excludedAssets = await _loadExcludedAssetIds(onDevice, excludedIds); - _log.info("Found ${excludedAssets.length} assets to exclude"); - } - // remove all excluded albums - onDevice.removeWhere((e) => excludedIds.contains(e.localId)); - _log.info("Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums"); - } - - final allAlbum = onDevice.firstWhereOrNull((album) => album.isAll); - final hasAll = allAlbum != null && selectedIds.contains(allAlbum.localId); - if (hasAll) { - if (Platform.isAndroid) { - // remove the virtual "Recent" album and keep and individual albums - // on Android, the virtual "Recent" `lastModified` value is always null - onDevice.removeWhere((album) => album.isAll); - _log.info("'Recents' is selected, keeping all individual albums"); - } - } else { - // keep only the explicitly selected albums - onDevice.removeWhere((album) => !selectedIds.contains(album.localId)); - _log.info("'Recents' is not selected, keeping only selected albums"); - } - changes = await _syncService.syncLocalAlbumAssetsToDb(onDevice, excludedAssets); - _log.info("Syncing completed. Changes: $changes"); - } finally { - _localCompleter.complete(changes); - } - dPrint(() => "refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms"); - return changes; - } - - Future> _loadExcludedAssetIds(List albums, Set excludedAlbumIds) async { - final Set result = HashSet(); - for (final batchAlbums in albums.where((album) => excludedAlbumIds.contains(album.localId)).slices(5)) { - await batchAlbums - .map((album) => _albumMediaRepository.getAssetIds(album.localId!).then((assetIds) => result.addAll(assetIds))) - .wait; - } - return result; - } - - /// Checks remote albums (owned if `isShared` is false) for changes, - /// updates the local database and returns `true` if there were any changes - Future refreshRemoteAlbums() async { - if (!_remoteCompleter.isCompleted) { - // guard against concurrent calls - return _remoteCompleter.future; - } - _remoteCompleter = Completer(); - final Stopwatch sw = Stopwatch()..start(); - bool changes = false; - try { - final users = await _syncService.getUsersFromServer(); - if (users != null) { - await _syncService.syncUsersFromServer(users); - } - final (sharedAlbum, ownedAlbum) = await ( - // Note: `shared: true` is required to get albums that don't belong to - // us due to unusual behaviour on the API but this will also return our - // own shared albums - _albumApiRepository.getAll(shared: true), - // Passing null (or nothing) for `shared` returns only albums that - // explicitly belong to us - _albumApiRepository.getAll(shared: null), - ).wait; - - final albums = HashSet(equals: (a, b) => a.remoteId == b.remoteId, hashCode: (a) => a.remoteId.hashCode); - - albums.addAll(sharedAlbum); - albums.addAll(ownedAlbum); - - changes = await _syncService.syncRemoteAlbumsToDb(albums.toList()); - } finally { - _remoteCompleter.complete(changes); - } - dPrint(() => "refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms"); - return changes; - } - - Future createAlbum( - String albumName, - Iterable assets, [ - Iterable sharedUsers = const [], - ]) async { - final Album album = await _albumApiRepository.create( - albumName, - assetIds: assets.map((asset) => asset.remoteId!), - sharedUserIds: sharedUsers.map((user) => user.id), - ); - await _entityService.fillAlbumWithDatabaseEntities(album); - return _albumRepository.create(album); - } - - /* - * Creates names like Untitled, Untitled (1), Untitled (2), ... - */ - Future _getNextAlbumName() async { - const baseName = "Untitled"; - for (int round = 0; ; round++) { - final proposedName = "$baseName${round == 0 ? "" : " ($round)"}"; - - if (null == await _albumRepository.getByName(proposedName, owner: true)) { - return proposedName; - } - } - } - - Future createAlbumWithGeneratedName(Iterable assets) async { - return createAlbum(await _getNextAlbumName(), assets, []); - } - - Future addAssets(Album album, Iterable assets) async { - try { - final result = await _albumApiRepository.addAssets(album.remoteId!, assets.map((asset) => asset.remoteId!)); - - final List addedAssets = result.added - .map((id) => assets.firstWhere((asset) => asset.remoteId == id)) - .toList(); - - await _updateAssets(album.id, add: addedAssets); - - return AlbumAddAssetsResponse(alreadyInAlbum: result.duplicates, successfullyAdded: addedAssets.length); - } catch (e) { - dPrint(() => "Error addAssets ${e.toString()}"); - } - return null; - } - - Future _updateAssets(int albumId, {List add = const [], List remove = const []}) => - _albumRepository.transaction(() async { - final album = await _albumRepository.get(albumId); - if (album == null) return; - await _albumRepository.addAssets(album, add); - await _albumRepository.removeAssets(album, remove); - await _albumRepository.recalculateMetadata(album); - await _albumRepository.update(album); - }); - - Future setActivityStatus(Album album, bool enabled) async { - try { - final updatedAlbum = await _albumApiRepository.update(album.remoteId!, activityEnabled: enabled); - album.activityEnabled = updatedAlbum.activityEnabled; - await _albumRepository.update(album); - return true; - } catch (e) { - dPrint(() => "Error setActivityEnabled ${e.toString()}"); - } - return false; - } - - Future deleteAlbum(Album album) async { - try { - final userId = _userService.getMyUser().id; - if (album.owner.value?.isarId == fastHash(userId)) { - await _albumApiRepository.delete(album.remoteId!); - } - if (album.shared) { - final foreignAssets = await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); - await _albumRepository.delete(album.id); - - final List albums = await _albumRepository.getAll(shared: true); - final List existing = []; - for (Album album in albums) { - existing.addAll(await _assetRepository.getByAlbum(album, notOwnedBy: [userId])); - } - final List idsToRemove = _syncService.sharedAssetsToRemove(foreignAssets, existing); - if (idsToRemove.isNotEmpty) { - await _assetRepository.deleteByIds(idsToRemove); - } - } else { - await _albumRepository.delete(album.id); - } - return true; - } catch (e) { - dPrint(() => "Error deleteAlbum ${e.toString()}"); - } - return false; - } - - Future leaveAlbum(Album album) async { - try { - await _albumApiRepository.removeUser(album.remoteId!, userId: "me"); - return true; - } catch (e) { - dPrint(() => "Error leaveAlbum ${e.toString()}"); - return false; - } - } - - Future removeAsset(Album album, Iterable assets) async { - try { - final result = await _albumApiRepository.removeAssets(album.remoteId!, assets.map((asset) => asset.remoteId!)); - final toRemove = result.removed.map((id) => assets.firstWhere((asset) => asset.remoteId == id)); - await _updateAssets(album.id, remove: toRemove.toList()); - return true; - } catch (e) { - dPrint(() => "Error removeAssetFromAlbum ${e.toString()}"); - } - return false; - } - - Future removeUser(Album album, UserDto user) async { - try { - await _albumApiRepository.removeUser(album.remoteId!, userId: user.id); - - album.sharedUsers.remove(entity.User.fromDto(user)); - await _albumRepository.removeUsers(album, [user]); - final a = await _albumRepository.get(album.id); - // trigger watcher - await _albumRepository.update(a!); - - return true; - } catch (error) { - dPrint(() => "Error removeUser ${error.toString()}"); - return false; - } - } - - Future addUsers(Album album, List userIds) async { - try { - final updatedAlbum = await _albumApiRepository.addUsers(album.remoteId!, userIds); - - album.sharedUsers.addAll(updatedAlbum.remoteUsers); - album.shared = true; - - await _albumRepository.addUsers(album, album.sharedUsers.map((u) => u.toDto()).toList()); - await _albumRepository.update(album); - - return true; - } catch (error) { - dPrint(() => "Error addUsers ${error.toString()}"); - } - return false; - } - - Future changeTitleAlbum(Album album, String newAlbumTitle) async { - try { - final updatedAlbum = await _albumApiRepository.update(album.remoteId!, name: newAlbumTitle); - - album.name = updatedAlbum.name; - await _albumRepository.update(album); - return true; - } catch (e) { - dPrint(() => "Error changeTitleAlbum ${e.toString()}"); - return false; - } - } - - Future changeDescriptionAlbum(Album album, String newAlbumDescription) async { - try { - final updatedAlbum = await _albumApiRepository.update(album.remoteId!, description: newAlbumDescription); - - album.description = updatedAlbum.description; - await _albumRepository.update(album); - return true; - } catch (e) { - dPrint(() => "Error changeDescriptionAlbum ${e.toString()}"); - return false; - } - } - - Future getAlbumByName(String name, {bool? remote, bool? shared, bool? owner}) => - _albumRepository.getByName(name, remote: remote, shared: shared, owner: owner); - - /// - /// Add the uploaded asset to the selected albums - /// - Future syncUploadAlbums(List albumNames, List assetIds) async { - for (final albumName in albumNames) { - Album? album = await getAlbumByName(albumName, remote: true, owner: true); - album ??= await createAlbum(albumName, []); - if (album != null && album.remoteId != null) { - await _albumApiRepository.addAssets(album.remoteId!, assetIds); - } - } - } - - Future> getAllRemoteAlbums() async { - return _albumRepository.getAll(remote: true); - } - - Future> getAllLocalAlbums() async { - return _albumRepository.getAll(remote: false); - } - - Stream> watchRemoteAlbums() { - return _albumRepository.watchRemoteAlbums(); - } - - Stream> watchLocalAlbums() { - return _albumRepository.watchLocalAlbums(); - } - - /// Get album by Isar ID - Future getAlbumById(int id) { - return _albumRepository.get(id); - } - - Future getAlbumByRemoteId(String remoteId) { - return _albumRepository.getByRemoteId(remoteId); - } - - Stream watchAlbum(int id) { - return _albumRepository.watchAlbum(id); - } - - Future> search(String searchTerm, QuickFilterMode filterMode) async { - return _albumRepository.search(searchTerm, filterMode); - } - - Future updateSortOrder(Album album, SortOrder order) async { - try { - final updateAlbum = await _albumApiRepository.update(album.remoteId!, sortOrder: order); - album.sortOrder = updateAlbum.sortOrder; - - return _albumRepository.update(album); - } catch (error, stackTrace) { - _log.severe("Error updating album sort order", error, stackTrace); - } - return null; - } - - Future clearTable() async { - await _albumRepository.clearTable(); - } -} diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index e296ac522d..ec4720f313 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -6,6 +6,7 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; +import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/url_helper.dart'; import 'package:logging/logging.dart'; @@ -184,8 +185,8 @@ class ApiService { if (externalJson != null) { final List list = jsonDecode(externalJson); for (final entry in list) { - final url = entry['url'] as String?; - if (url != null && url.isNotEmpty) urls.add(url); + final url = AuxilaryEndpoint.fromJson(entry).url; + if (url.isNotEmpty) urls.add(url); } } return urls; diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart deleted file mode 100644 index b9fab35442..0000000000 --- a/mobile/lib/services/asset.service.dart +++ /dev/null @@ -1,465 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package: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( - ref.watch(assetApiRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(exifRepositoryProvider), - ref.watch(userRepositoryProvider), - ref.watch(etagRepositoryProvider), - ref.watch(backupAlbumRepositoryProvider), - ref.watch(apiServiceProvider), - ref.watch(syncServiceProvider), - ref.watch(backupServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(userServiceProvider), - ref.watch(assetMediaRepositoryProvider), - ), -); - -class AssetService { - final AssetApiRepository _assetApiRepository; - final AssetRepository _assetRepository; - final IsarExifRepository _exifInfoRepository; - final IsarUserRepository _isarUserRepository; - final ETagRepository _etagRepository; - final BackupAlbumRepository _backupRepository; - final ApiService _apiService; - final SyncService _syncService; - final BackupService _backupService; - final AlbumService _albumService; - final UserService _userService; - final AssetMediaRepository _assetMediaRepository; - final log = Logger('AssetService'); - - AssetService( - this._assetApiRepository, - this._assetRepository, - this._exifInfoRepository, - this._isarUserRepository, - this._etagRepository, - this._backupRepository, - this._apiService, - this._syncService, - this._backupService, - this._albumService, - this._userService, - this._assetMediaRepository, - ); - - /// Checks the server for updated assets and updates the local database if - /// required. Returns `true` if there were any changes. - Future refreshRemoteAssets() async { - final syncedUserIds = await _etagRepository.getAllIds(); - final List syncedUsers = syncedUserIds.isEmpty - ? [] - : (await _isarUserRepository.getByUserIds(syncedUserIds)).nonNulls.toList(); - final Stopwatch sw = Stopwatch()..start(); - final bool changes = await _syncService.syncRemoteAssetsToDb( - users: syncedUsers, - getChangedAssets: _getRemoteAssetChanges, - loadAssets: _getRemoteAssets, - ); - dPrint(() => "refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms"); - return changes; - } - - /// Returns `(null, null)` if changes are invalid -> requires full sync - Future<(List? toUpsert, List? toDelete)> _getRemoteAssetChanges( - List users, - DateTime since, - ) async { - final dto = AssetDeltaSyncDto(updatedAfter: since, userIds: users.map((e) => e.id).toList()); - final changes = await _apiService.syncApi.getDeltaSync(dto); - return changes == null || changes.needsFullSync - ? (null, null) - : (changes.upserted.map(Asset.remote).toList(), changes.deleted); - } - - /// Returns the list of people of the given asset id. - // If the server is not reachable `null` is returned. - Future?> getRemotePeopleOfAsset(String remoteId) async { - try { - final AssetResponseDto? dto = await _apiService.assetsApi.getAssetInfo(remoteId); - - return dto?.people; - } catch (error, stack) { - log.severe('Error while getting remote asset info: ${error.toString()}', error, stack); - - return null; - } - } - - /// Returns `null` if the server state did not change, else list of assets - Future?> _getRemoteAssets(UserDto user, DateTime until) async { - const int chunkSize = 10000; - try { - final List allAssets = []; - String? lastId; - // will break on error or once all assets are loaded - while (true) { - final dto = AssetFullSyncDto(limit: chunkSize, updatedUntil: until, lastId: lastId, userId: user.id); - log.fine("Requesting $chunkSize assets from $lastId"); - final List? assets = await _apiService.syncApi.getFullSyncForUser(dto); - if (assets == null) return null; - log.fine("Received ${assets.length} assets from ${assets.firstOrNull?.id} to ${assets.lastOrNull?.id}"); - allAssets.addAll(assets.map(Asset.remote)); - if (assets.length != chunkSize) break; - lastId = assets.last.id; - } - return allAssets; - } catch (error, stack) { - log.severe('Error while getting remote assets', error, stack); - return null; - } - } - - /// Loads the exif information from the database. If there is none, loads - /// the exif info from the server (remote assets only) - Future loadExif(Asset a) async { - a.exifInfo ??= (await _exifInfoRepository.get(a.id)); - // fileSize is always filled on the server but not set on client - if (a.exifInfo?.fileSize == null) { - if (a.isRemote) { - final dto = await _apiService.assetsApi.getAssetInfo(a.remoteId!); - if (dto != null && dto.exifInfo != null) { - final newExif = Asset.remote(dto).exifInfo!.copyWith(assetId: a.id); - a.exifInfo = newExif; - if (newExif != a.exifInfo) { - if (a.isInDb) { - await _assetRepository.transaction(() => _assetRepository.update(a)); - } else { - dPrint(() => "[loadExif] parameter Asset is not from DB!"); - } - } - } - } else { - // TODO implement local exif info parsing - } - } - return a; - } - - Future updateAssets(List assets, UpdateAssetDto updateAssetDto) async { - return await _apiService.assetsApi.updateAssets( - AssetBulkUpdateDto( - ids: assets.map((e) => e.remoteId!).toList(), - dateTimeOriginal: updateAssetDto.dateTimeOriginal, - isFavorite: updateAssetDto.isFavorite, - visibility: updateAssetDto.visibility, - latitude: updateAssetDto.latitude, - longitude: updateAssetDto.longitude, - ), - ); - } - - Future> changeFavoriteStatus(List assets, bool isFavorite) async { - try { - await updateAssets(assets, UpdateAssetDto(isFavorite: isFavorite)); - - for (var element in assets) { - element.isFavorite = isFavorite; - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing favorite status", error, stack); - return []; - } - } - - Future> changeArchiveStatus(List assets, bool isArchived) async { - try { - await updateAssets( - assets, - UpdateAssetDto(visibility: isArchived ? AssetVisibility.archive : AssetVisibility.timeline), - ); - - for (var element in assets) { - element.isArchived = isArchived; - element.visibility = isArchived ? AssetVisibilityEnum.archive : AssetVisibilityEnum.timeline; - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing archive status", error, stack); - return []; - } - } - - Future?> changeDateTime(List assets, String updatedDt) async { - try { - await updateAssets(assets, UpdateAssetDto(dateTimeOriginal: updatedDt)); - - for (var element in assets) { - element.fileCreatedAt = DateTime.parse(updatedDt); - element.exifInfo = element.exifInfo?.copyWith(dateTimeOriginal: DateTime.parse(updatedDt)); - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing date/time status", error, stack); - return Future.value(null); - } - } - - Future?> changeLocation(List assets, LatLng location) async { - try { - await updateAssets(assets, UpdateAssetDto(latitude: location.latitude, longitude: location.longitude)); - - for (var element in assets) { - element.exifInfo = element.exifInfo?.copyWith(latitude: location.latitude, longitude: location.longitude); - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing location status", error, stack); - return Future.value(null); - } - } - - Future syncUploadedAssetToAlbums() async { - try { - final selectedAlbums = await _backupRepository.getAllBySelection(BackupSelection.select); - final excludedAlbums = await _backupRepository.getAllBySelection(BackupSelection.exclude); - - final candidates = await _backupService.buildUploadCandidates( - selectedAlbums, - excludedAlbums, - useTimeFilter: false, - ); - - await refreshRemoteAssets(); - final owner = _userService.getMyUser(); - final remoteAssets = await _assetRepository.getAll(ownerId: owner.id, state: AssetState.merged); - - /// Map - Map> assetToAlbums = {}; - - for (BackupCandidate candidate in candidates) { - final asset = remoteAssets.firstWhereOrNull((a) => a.localId == candidate.asset.localId); - - if (asset != null) { - for (final albumName in candidate.albumNames) { - assetToAlbums.putIfAbsent(albumName, () => []).add(asset.remoteId!); - } - } - } - - // Upload assets to albums - for (final entry in assetToAlbums.entries) { - final albumName = entry.key; - final assetIds = entry.value; - - await _albumService.syncUploadAlbums([albumName], assetIds); - } - } catch (error, stack) { - log.severe("Error while syncing uploaded asset to albums", error, stack); - } - } - - Future setDescription(Asset asset, String newDescription) async { - final remoteAssetId = asset.remoteId; - final localExifId = asset.exifInfo?.assetId; - - // Guard [remoteAssetId] and [localExifId] null - if (remoteAssetId == null || localExifId == null) { - return; - } - - final result = await _assetApiRepository.update(remoteAssetId, description: newDescription); - - final description = result.exifInfo?.description; - - if (description != null) { - var exifInfo = await _exifInfoRepository.get(localExifId); - - if (exifInfo != null) { - await _exifInfoRepository.update(exifInfo.copyWith(description: description)); - } - } - } - - Future getDescription(Asset asset) async { - final localExifId = asset.exifInfo?.assetId; - - // Guard [remoteAssetId] and [localExifId] null - if (localExifId == null) { - return ""; - } - - final exifInfo = await _exifInfoRepository.get(localExifId); - - return exifInfo?.description ?? ""; - } - - Future getAspectRatio(Asset asset) async { - if (asset.isRemote) { - asset = await loadExif(asset); - } else if (asset.isLocal) { - await asset.localAsync; - } - - final aspectRatio = asset.aspectRatio; - if (aspectRatio != null) { - return aspectRatio; - } - - final width = asset.width; - final height = asset.height; - if (width != null && height != null) { - // we don't know the orientation, so assume it's normal - return width / height; - } - - return 1.0; - } - - Future> getStackAssets(String stackId) { - return _assetRepository.getStackAssets(stackId); - } - - Future clearTable() { - return _assetRepository.clearTable(); - } - - /// Delete assets from local file system and unreference from the database - Future deleteLocalAssets(Iterable assets) async { - // Delete files from local gallery - final candidates = assets.where((asset) => asset.isLocal); - - final deletedIds = await _assetMediaRepository.deleteAll(candidates.map((asset) => asset.localId!).toList()); - - // Modify local database by removing the reference to the local assets - if (deletedIds.isNotEmpty) { - // Delete records from local database - final isarIds = assets.where((asset) => asset.storage == AssetState.local).map((asset) => asset.id).toList(); - await _assetRepository.deleteByIds(isarIds); - - // Modify Merged asset to be remote only - final updatedAssets = assets.where((asset) => asset.storage == AssetState.merged).map((asset) { - asset.localId = null; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } - } - - /// Delete assets from the server and unreference from the database - Future deleteRemoteAssets(Iterable assets, {bool shouldDeletePermanently = false}) async { - final candidates = assets.where((a) => a.isRemote); - - if (candidates.isEmpty) { - return; - } - - await _apiService.assetsApi.deleteAssets( - AssetBulkDeleteDto(ids: candidates.map((a) => a.remoteId!).toList(), force: shouldDeletePermanently), - ); - - /// Update asset info bassed on the deletion type. - final payload = shouldDeletePermanently - ? assets.where((asset) => asset.storage == AssetState.merged).map((asset) { - asset.remoteId = null; - asset.visibility = AssetVisibilityEnum.timeline; - return asset; - }) - : assets.where((asset) => asset.isRemote).map((asset) { - asset.isTrashed = true; - return asset; - }); - - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(payload.toList()); - - if (shouldDeletePermanently) { - final remoteAssetIds = assets - .where((asset) => asset.storage == AssetState.remote) - .map((asset) => asset.id) - .toList(); - await _assetRepository.deleteByIds(remoteAssetIds); - } - }); - } - - /// Delete assets on both local file system and the server. - /// Unreference from the database. - Future deleteAssets(Iterable assets, {bool shouldDeletePermanently = false}) async { - final hasLocal = assets.any((asset) => asset.isLocal); - final hasRemote = assets.any((asset) => asset.isRemote); - - if (hasLocal) { - await deleteLocalAssets(assets); - } - - if (hasRemote) { - await deleteRemoteAssets(assets, shouldDeletePermanently: shouldDeletePermanently); - } - } - - Stream watchAsset(int id, {bool fireImmediately = false}) { - return _assetRepository.watchAsset(id, fireImmediately: fireImmediately); - } - - Future> getRecentlyTakenAssets() { - final me = _userService.getMyUser(); - return _assetRepository.getRecentlyTakenAssets(me.id); - } - - Future> getMotionAssets() { - final me = _userService.getMyUser(); - return _assetRepository.getMotionAssets(me.id); - } - - Future setVisibility(List assets, AssetVisibilityEnum visibility) async { - await _assetApiRepository.updateVisibility(assets.map((asset) => asset.remoteId!).toList(), visibility); - - final updatedAssets = assets.map((asset) { - asset.visibility = visibility; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } - - Future getAssetByRemoteId(String remoteId) async { - final assets = await _assetRepository.getAllByRemoteId([remoteId]); - return assets.isNotEmpty ? assets.first : null; - } -} diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart index c5f3fa6a4a..667681e579 100644 --- a/mobile/lib/services/auth.service.dart +++ b/mobile/lib/services/auth.service.dart @@ -67,6 +67,9 @@ class AuthService { bool isValid = false; try { + final urls = ApiService.getServerUrls(); + urls.add(url); + await NetworkRepository.setHeaders(ApiService.getRequestHeaders(), urls); final uri = Uri.parse('$url/users/me'); final response = await NetworkRepository.client.get(uri); if (response.statusCode == 200) { diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart deleted file mode 100644 index 03278d25fc..0000000000 --- a/mobile/lib/services/background.service.dart +++ /dev/null @@ -1,595 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; -import 'dart:io'; -import 'dart:isolate'; -import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.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/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/repositories/backup.repository.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/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:path_provider_foundation/path_provider_foundation.dart'; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; - -final backgroundServiceProvider = Provider((ref) => BackgroundService()); - -/// Background backup service -class BackgroundService { - static const String _portNameLock = "immichLock"; - static const MethodChannel _foregroundChannel = MethodChannel('immich/foregroundChannel'); - static const MethodChannel _backgroundChannel = MethodChannel('immich/backgroundChannel'); - static const notifyInterval = Duration(milliseconds: 400); - bool _isBackgroundInitialized = false; - Completer? _cancellationToken; - bool _canceledBySystem = false; - int _wantsLockTime = 0; - bool _hasLock = false; - SendPort? _waitingIsolate; - ReceivePort? _rp; - bool _errorGracePeriodExceeded = true; - int _uploadedAssetsCount = 0; - int _assetsToUploadCount = 0; - String _lastPrintedDetailContent = ""; - String? _lastPrintedDetailTitle; - late final ThrottleProgressUpdate _throttledNotifiy = ThrottleProgressUpdate(_updateProgress, notifyInterval); - late final ThrottleProgressUpdate _throttledDetailNotify = ThrottleProgressUpdate( - _updateDetailProgress, - notifyInterval, - ); - - bool get isBackgroundInitialized { - return _isBackgroundInitialized; - } - - /// Ensures that the background service is enqueued if enabled in settings - Future resumeServiceIfEnabled() async { - return await isBackgroundBackupEnabled() && await enableService(); - } - - /// Enqueues the background service - Future enableService({bool immediate = false}) async { - try { - final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!; - final String title = "backup_background_service_default_notification".tr(); - final bool ok = await _foregroundChannel.invokeMethod('enable', [callback.toRawHandle(), title, immediate]); - return ok; - } catch (error) { - return false; - } - } - - /// Configures the background service - Future configureService({ - bool requireUnmetered = true, - bool requireCharging = false, - int triggerUpdateDelay = 5000, - int triggerMaxDelay = 50000, - }) async { - try { - final bool ok = await _foregroundChannel.invokeMethod('configure', [ - requireUnmetered, - requireCharging, - triggerUpdateDelay, - triggerMaxDelay, - ]); - return ok; - } catch (error) { - return false; - } - } - - /// Cancels the background service (if currently running) and removes it from work queue - Future disableService() async { - try { - final ok = await _foregroundChannel.invokeMethod('disable'); - return ok; - } catch (error) { - return false; - } - } - - /// Returns `true` if the background service is enabled - Future isBackgroundBackupEnabled() async { - try { - return await _foregroundChannel.invokeMethod("isEnabled"); - } catch (error) { - return false; - } - } - - /// Returns `true` if battery optimizations are disabled - Future isIgnoringBatteryOptimizations() async { - // iOS does not need battery optimizations enabled - if (Platform.isIOS) { - return true; - } - try { - return await _foregroundChannel.invokeMethod('isIgnoringBatteryOptimizations'); - } catch (error) { - return false; - } - } - - // Yet to be implemented - Future digestFile(String path) { - return _foregroundChannel.invokeMethod("digestFile", [path]); - } - - Future?> digestFiles(List paths) { - return _foregroundChannel.invokeListMethod("digestFiles", paths); - } - - /// Updates the notification shown by the background service - Future _updateNotification({ - String? title, - String? content, - int progress = 0, - int max = 0, - bool indeterminate = false, - bool isDetail = false, - bool onlyIfFG = false, - }) async { - try { - if (_isBackgroundInitialized) { - return _backgroundChannel.invokeMethod('updateNotification', [ - title, - content, - progress, - max, - indeterminate, - isDetail, - onlyIfFG, - ]); - } - } catch (error) { - dPrint(() => "[_updateNotification] failed to communicate with plugin"); - } - return false; - } - - /// Shows a new priority notification - Future _showErrorNotification({required String title, String? content, String? individualTag}) async { - try { - if (_isBackgroundInitialized && _errorGracePeriodExceeded) { - return await _backgroundChannel.invokeMethod('showError', [title, content, individualTag]); - } - } catch (error) { - dPrint(() => "[_showErrorNotification] failed to communicate with plugin"); - } - return false; - } - - Future _clearErrorNotifications() async { - try { - if (_isBackgroundInitialized) { - return await _backgroundChannel.invokeMethod('clearErrorNotifications'); - } - } catch (error) { - dPrint(() => "[_clearErrorNotifications] failed to communicate with plugin"); - } - return false; - } - - /// await to ensure this thread (foreground or background) has exclusive access - Future acquireLock() async { - if (_hasLock) { - dPrint(() => "WARNING: [acquireLock] called more than once"); - return true; - } - final int lockTime = Timeline.now; - _wantsLockTime = lockTime; - final ReceivePort rp = ReceivePort(_portNameLock); - _rp = rp; - final SendPort sp = rp.sendPort; - - while (!IsolateNameServer.registerPortWithName(sp, _portNameLock)) { - try { - await _checkLockReleasedWithHeartbeat(lockTime); - } catch (error) { - return false; - } - if (_wantsLockTime != lockTime) { - return false; - } - } - _hasLock = true; - rp.listen(_heartbeatListener); - return true; - } - - Future _checkLockReleasedWithHeartbeat(final int lockTime) async { - SendPort? other = IsolateNameServer.lookupPortByName(_portNameLock); - if (other != null) { - final ReceivePort tempRp = ReceivePort(); - final SendPort tempSp = tempRp.sendPort; - final bs = tempRp.asBroadcastStream(); - while (_wantsLockTime == lockTime) { - other.send(tempSp); - final dynamic answer = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => null); - if (_wantsLockTime != lockTime) { - break; - } - if (answer == null) { - // other isolate failed to answer, assuming it exited without releasing the lock - if (other == IsolateNameServer.lookupPortByName(_portNameLock)) { - IsolateNameServer.removePortNameMapping(_portNameLock); - } - break; - } else if (answer == true) { - // other isolate released the lock - break; - } else if (answer == false) { - // other isolate is still active - } - final dynamic isFinished = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => false); - if (isFinished == true) { - break; - } - } - tempRp.close(); - } - } - - void _heartbeatListener(dynamic msg) { - if (msg is SendPort) { - _waitingIsolate = msg; - msg.send(false); - } - } - - /// releases the exclusive access lock - void releaseLock() { - _wantsLockTime = 0; - if (_hasLock) { - IsolateNameServer.removePortNameMapping(_portNameLock); - _waitingIsolate?.send(true); - _waitingIsolate = null; - _hasLock = false; - } - _rp?.close(); - _rp = null; - } - - void _setupBackgroundCallHandler() { - _backgroundChannel.setMethodCallHandler(_callHandler); - _isBackgroundInitialized = true; - _backgroundChannel.invokeMethod('initialized'); - } - - Future _callHandler(MethodCall call) async { - DartPluginRegistrant.ensureInitialized(); - if (Platform.isIOS) { - // NOTE: I'm not sure this is strictly necessary anymore, but - // out of an abundance of caution, we will keep it in until someone - // can say for sure - PathProviderFoundation.registerWith(); - } - switch (call.method) { - case "backgroundProcessing": - case "onAssetsChanged": - try { - unawaited(_clearErrorNotifications()); - - // iOS should time out after some threshold so it doesn't wait - // indefinitely and can run later - // Android is fine to wait here until the lock releases - final waitForLock = Platform.isIOS - ? acquireLock().timeout(const Duration(seconds: 5), onTimeout: () => false) - : acquireLock(); - - final bool hasAccess = await waitForLock; - if (!hasAccess) { - dPrint(() => "[_callHandler] could not acquire lock, exiting"); - return false; - } - - final translationsOk = await loadTranslations(); - if (!translationsOk) { - dPrint(() => "[_callHandler] could not load translations"); - } - - final bool ok = await _onAssetsChanged(); - return ok; - } catch (error) { - dPrint(() => error.toString()); - return false; - } finally { - releaseLock(); - } - case "systemStop": - _canceledBySystem = true; - _cancellationToken?.complete(); - _cancellationToken = null; - return true; - default: - dPrint(() => "Unknown method ${call.method}"); - return false; - } - } - - Future _onAssetsChanged() async { - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false); - - final ref = ProviderContainer( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - ); - - await ref.read(authServiceProvider).setOpenApiServiceEndpoint(); - 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); - if (selectedAlbums.isEmpty) { - return true; - } - - await ref.read(fileMediaRepositoryProvider).enableBackgroundAccess(); - - do { - final bool backupOk = await _runBackup( - ref.read(backupServiceProvider), - ref.read(appSettingsServiceProvider), - selectedAlbums, - excludedAlbums, - ); - if (backupOk) { - await Store.delete(StoreKey.backupFailedSince); - final backupAlbums = [...selectedAlbums, ...excludedAlbums]; - backupAlbums.sortBy((e) => e.id); - - final dbAlbums = await ref.read(backupAlbumRepositoryProvider).getAll(sort: BackupAlbumSort.id); - final List toDelete = []; - final List toUpsert = []; - // stores the most recent `lastBackup` per album but always keeps the `selection` from the most recent DB state - diffSortedListsSync( - dbAlbums, - backupAlbums, - compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id), - both: (BackupAlbum a, BackupAlbum b) { - a.lastBackup = a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup; - toUpsert.add(a); - return true; - }, - onlyFirst: (BackupAlbum a) => toUpsert.add(a), - onlySecond: (BackupAlbum b) => toDelete.add(b.isarId), - ); - await ref.read(backupAlbumRepositoryProvider).deleteAll(toDelete); - await ref.read(backupAlbumRepositoryProvider).updateAll(toUpsert); - } else if (Store.tryGet(StoreKey.backupFailedSince) == null) { - await Store.put(StoreKey.backupFailedSince, DateTime.now()); - return false; - } - // Android should check for new assets added while performing backup - } while (Platform.isAndroid && true == await _backgroundChannel.invokeMethod("hasContentChanged")); - return true; - } - - Future _runBackup( - BackupService backupService, - AppSettingsService settingsService, - List selectedAlbums, - List excludedAlbums, - ) async { - _errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService); - final bool notifyTotalProgress = settingsService.getSetting(AppSettingsEnum.backgroundBackupTotalProgress); - final bool notifySingleProgress = settingsService.getSetting(AppSettingsEnum.backgroundBackupSingleProgress); - - if (_canceledBySystem) { - return false; - } - - Set toUpload = await backupService.buildUploadCandidates(selectedAlbums, excludedAlbums); - - try { - toUpload = await backupService.removeAlreadyUploadedAssets(toUpload); - } catch (e) { - unawaited( - _showErrorNotification( - title: "backup_background_service_error_title".tr(), - content: "backup_background_service_connection_failed_message".tr(), - ), - ); - return false; - } - - if (_canceledBySystem) { - return false; - } - - if (toUpload.isEmpty) { - return true; - } - _assetsToUploadCount = toUpload.length; - _uploadedAssetsCount = 0; - unawaited( - _updateNotification( - title: "backup_background_service_in_progress_notification".tr(), - content: notifyTotalProgress ? formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount) : null, - progress: 0, - max: notifyTotalProgress ? _assetsToUploadCount : 0, - indeterminate: !notifyTotalProgress, - onlyIfFG: !notifyTotalProgress, - ), - ); - - _cancellationToken?.complete(); - _cancellationToken = Completer(); - final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - - final bool ok = await backupService.backupAsset( - toUpload, - _cancellationToken!, - pmProgressHandler: pmProgressHandler, - onSuccess: (result) => _onAssetUploaded(shouldNotify: notifyTotalProgress), - onProgress: (bytes, totalBytes) => _onProgress(bytes, totalBytes, shouldNotify: notifySingleProgress), - onCurrentAsset: (asset) => _onSetCurrentBackupAsset(asset, shouldNotify: notifySingleProgress), - onError: _onBackupError, - isBackground: true, - ); - - if (!ok && !_cancellationToken!.isCompleted) { - unawaited( - _showErrorNotification( - title: "backup_background_service_error_title".tr(), - content: "backup_background_service_backup_failed_message".tr(), - ), - ); - } - - return ok; - } - - void _onAssetUploaded({bool shouldNotify = false}) { - if (!shouldNotify) { - return; - } - - _uploadedAssetsCount++; - _throttledNotifiy(); - } - - void _onProgress(int bytes, int totalBytes, {bool shouldNotify = false}) { - if (!shouldNotify) { - return; - } - - _throttledDetailNotify(progress: bytes, total: totalBytes); - } - - void _updateDetailProgress(String? title, int progress, int total) { - final String msg = total > 0 ? humanReadableBytesProgress(progress, total) : ""; - // only update if message actually differs (to stop many useless notification updates on large assets or slow connections) - if (msg != _lastPrintedDetailContent || _lastPrintedDetailTitle != title) { - _lastPrintedDetailContent = msg; - _lastPrintedDetailTitle = title; - _updateNotification( - progress: total > 0 ? (progress * 1000) ~/ total : 0, - max: 1000, - isDetail: true, - title: title, - content: msg, - ); - } - } - - void _updateProgress(String? title, int progress, int total) { - _updateNotification( - progress: _uploadedAssetsCount, - max: _assetsToUploadCount, - title: title, - content: formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount), - ); - } - - void _onBackupError(ErrorUploadAsset errorAssetInfo) { - _showErrorNotification( - title: "backup_background_service_upload_failure_notification".tr( - namedArgs: {'filename': errorAssetInfo.fileName}, - ), - individualTag: errorAssetInfo.id, - ); - } - - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset, {bool shouldNotify = false}) { - if (!shouldNotify) { - return; - } - - _throttledDetailNotify.title = "backup_background_service_current_upload_notification".tr( - namedArgs: {'filename': currentUploadAsset.fileName}, - ); - _throttledDetailNotify.progress = 0; - _throttledDetailNotify.total = 0; - } - - bool _isErrorGracePeriodExceeded(AppSettingsService appSettingsService) { - final int value = appSettingsService.getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod); - if (value == 0) { - return true; - } else if (value == 5) { - return false; - } - final DateTime? failedSince = Store.tryGet(StoreKey.backupFailedSince); - if (failedSince == null) { - return false; - } - final Duration duration = DateTime.now().difference(failedSince); - if (value == 1) { - return duration > const Duration(minutes: 30); - } else if (value == 2) { - return duration > const Duration(hours: 2); - } else if (value == 3) { - return duration > const Duration(hours: 8); - } else if (value == 4) { - return duration > const Duration(hours: 24); - } - assert(false, "Invalid value"); - return true; - } - - Future getIOSBackupLastRun(IosBackgroundTask task) async { - if (!Platform.isIOS) { - return null; - } - // Seconds since last run - final double? lastRun = task == IosBackgroundTask.fetch - ? await _foregroundChannel.invokeMethod('lastBackgroundFetchTime') - : await _foregroundChannel.invokeMethod('lastBackgroundProcessingTime'); - if (lastRun == null) { - return null; - } - final time = DateTime.fromMillisecondsSinceEpoch(lastRun.toInt() * 1000); - return time; - } - - Future getIOSBackupNumberOfProcesses() async { - if (!Platform.isIOS) { - return 0; - } - return await _foregroundChannel.invokeMethod('numberOfBackgroundProcesses'); - } - - Future getIOSBackgroundAppRefreshEnabled() async { - if (!Platform.isIOS) { - return false; - } - return await _foregroundChannel.invokeMethod('backgroundAppRefreshEnabled'); - } -} - -enum IosBackgroundTask { fetch, processing } - -/// entry point called by Kotlin/Java code; needs to be a top-level function -@pragma('vm:entry-point') -void _nativeEntry() { - WidgetsFlutterBinding.ensureInitialized(); - DartPluginRegistrant.ensureInitialized(); - BackgroundService backgroundService = BackgroundService(); - backgroundService._setupBackgroundCallHandler(); -} diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart deleted file mode 100644 index 9b6a26be03..0000000000 --- a/mobile/lib/services/backup.service.dart +++ /dev/null @@ -1,473 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:http/http.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; -import 'package:immich_mobile/repositories/upload.repository.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:logging/logging.dart'; -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( - ref.watch(apiServiceProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(assetMediaRepositoryProvider), - ), -); - -class BackupService { - final ApiService _apiService; - final Logger _log = Logger("BackupService"); - final AppSettingsService _appSetting; - final AlbumService _albumService; - final AlbumMediaRepository _albumMediaRepository; - final FileMediaRepository _fileMediaRepository; - final AssetRepository _assetRepository; - final AssetMediaRepository _assetMediaRepository; - - BackupService( - this._apiService, - this._appSetting, - this._albumService, - this._albumMediaRepository, - this._fileMediaRepository, - this._assetRepository, - this._assetMediaRepository, - ); - - Future?> getDeviceBackupAsset() async { - final String deviceId = Store.get(StoreKey.deviceId); - - try { - return await _apiService.assetsApi.getAllUserAssetsByDeviceId(deviceId); - } catch (e) { - dPrint(() => 'Error [getDeviceBackupAsset] ${e.toString()}'); - return null; - } - } - - Future _saveDuplicatedAssetIds(List deviceAssetIds) => - _assetRepository.transaction(() => _assetRepository.upsertDuplicatedAssets(deviceAssetIds)); - - /// Get duplicated asset id from database - Future> getDuplicatedAssetIds() async { - final duplicates = await _assetRepository.getAllDuplicatedAssetIds(); - return duplicates.toSet(); - } - - /// Returns all assets newer than the last successful backup per album - /// if `useTimeFilter` is set to true, all assets will be returned - Future> buildUploadCandidates( - List selectedBackupAlbums, - List excludedBackupAlbums, { - bool useTimeFilter = true, - }) async { - final now = DateTime.now(); - - final Set toAdd = await _fetchAssetsAndUpdateLastBackup( - selectedBackupAlbums, - now, - useTimeFilter: useTimeFilter, - ); - - if (toAdd.isEmpty) return {}; - - final Set toRemove = await _fetchAssetsAndUpdateLastBackup( - excludedBackupAlbums, - now, - useTimeFilter: useTimeFilter, - ); - - return toAdd.difference(toRemove); - } - - Future> _fetchAssetsAndUpdateLastBackup( - List backupAlbums, - DateTime now, { - bool useTimeFilter = true, - }) async { - Set candidates = {}; - - for (final BackupAlbum backupAlbum in backupAlbums) { - final Album localAlbum; - try { - localAlbum = await _albumMediaRepository.get(backupAlbum.id); - } on StateError { - // the album no longer exists - continue; - } - - if (useTimeFilter && localAlbum.modifiedAt.isBefore(backupAlbum.lastBackup)) { - continue; - } - final List assets; - try { - assets = await _albumMediaRepository.getAssets( - backupAlbum.id, - modifiedFrom: useTimeFilter - ? - // subtract 2 seconds to prevent missing assets due to rounding issues - backupAlbum.lastBackup.subtract(const Duration(seconds: 2)) - : null, - modifiedUntil: useTimeFilter ? now : null, - ); - } on StateError { - // either there are no assets matching the filter criteria OR the album no longer exists - continue; - } - - // Add album's name to the asset info - for (final asset in assets) { - List albumNames = [localAlbum.name]; - - final existingAsset = candidates.firstWhereOrNull((candidate) => candidate.asset.localId == asset.localId); - - if (existingAsset != null) { - albumNames.addAll(existingAsset.albumNames); - candidates.remove(existingAsset); - } - - candidates.add(BackupCandidate(asset: asset, albumNames: albumNames)); - } - - backupAlbum.lastBackup = now; - } - - return candidates; - } - - /// Returns a new list of assets not yet uploaded - Future> removeAlreadyUploadedAssets(Set candidates) async { - if (candidates.isEmpty) { - return candidates; - } - - final Set duplicatedAssetIds = await getDuplicatedAssetIds(); - candidates.removeWhere((candidate) => duplicatedAssetIds.contains(candidate.asset.localId)); - - if (candidates.isEmpty) { - return candidates; - } - - final Set existing = {}; - try { - final String deviceId = Store.get(StoreKey.deviceId); - final CheckExistingAssetsResponseDto? duplicates = await _apiService.assetsApi.checkExistingAssets( - CheckExistingAssetsDto(deviceAssetIds: candidates.map((c) => c.asset.localId!).toList(), deviceId: deviceId), - ); - if (duplicates != null) { - existing.addAll(duplicates.existingIds); - } - } on ApiException { - // workaround for older server versions or when checking for too many assets at once - final List? allAssetsInDatabase = await getDeviceBackupAsset(); - if (allAssetsInDatabase != null) { - existing.addAll(allAssetsInDatabase); - } - } - - if (existing.isNotEmpty) { - candidates.removeWhere((c) => existing.contains(c.asset.localId)); - } - - return candidates; - } - - Future _checkPermissions() async { - if (Platform.isAndroid && !(await pm.Permission.accessMediaLocation.status).isGranted) { - // double check that permission is granted here, to guard against - // uploading corrupt assets without EXIF information - _log.warning( - "Media location permission is not granted. " - "Cannot access original assets for backup.", - ); - - return false; - } - - // DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS - if (Platform.isIOS) { - await _fileMediaRepository.requestExtendedPermissions(); - } - - return true; - } - - /// Upload images before video assets for background tasks - /// these are further sorted by using their creation date - List _sortPhotosFirst(List candidates) { - return candidates.sorted((a, b) { - final cmp = a.asset.type.index - b.asset.type.index; - if (cmp != 0) return cmp; - return a.asset.fileCreatedAt.compareTo(b.asset.fileCreatedAt); - }); - } - - Future backupAsset( - Iterable assets, - Completer cancelToken, { - bool isBackground = false, - PMProgressHandler? pmProgressHandler, - required void Function(SuccessUploadAsset result) onSuccess, - required void Function(int bytes, int totalBytes) onProgress, - required void Function(CurrentUploadAsset asset) onCurrentAsset, - required void Function(ErrorUploadAsset error) onError, - }) async { - final bool isIgnoreIcloudAssets = _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets); - final shouldSyncAlbums = _appSetting.getSetting(AppSettingsEnum.syncAlbums); - final String deviceId = Store.get(StoreKey.deviceId); - final String savedEndpoint = Store.get(StoreKey.serverEndpoint); - final List duplicatedAssetIds = []; - bool anyErrors = false; - - final hasPermission = await _checkPermissions(); - if (!hasPermission) { - return false; - } - - List candidates = assets.toList(); - if (isBackground) { - candidates = _sortPhotosFirst(candidates); - } - - for (final candidate in candidates) { - final Asset asset = candidate.asset; - File? file; - File? livePhotoFile; - - try { - final isAvailableLocally = await asset.local!.isLocallyAvailable(isOrigin: true); - - // Handle getting files from iCloud - if (!isAvailableLocally && Platform.isIOS) { - // Skip iCloud assets if the user has disabled this feature - if (isIgnoreIcloudAssets) { - continue; - } - - onCurrentAsset( - CurrentUploadAsset( - id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt.year == 1970 ? asset.fileModifiedAt : asset.fileCreatedAt, - fileName: asset.fileName, - fileType: _getAssetType(asset.type), - iCloudAsset: true, - ), - ); - - file = await asset.local!.loadFile(progressHandler: pmProgressHandler); - if (asset.local!.isLivePhoto) { - livePhotoFile = await asset.local!.loadFile(withSubtype: true, progressHandler: pmProgressHandler); - } - } else { - file = await asset.local!.originFile.timeout(const Duration(seconds: 5)); - - if (asset.local!.isLivePhoto) { - livePhotoFile = await asset.local!.originFileWithSubtype.timeout(const Duration(seconds: 5)); - } - } - - if (file != null) { - String? originalFileName = await _assetMediaRepository.getOriginalFilename(asset.localId!); - originalFileName ??= asset.fileName; - - if (asset.local!.isLivePhoto) { - if (livePhotoFile == null) { - _log.warning("Failed to obtain motion part of the livePhoto - $originalFileName"); - } - } - - final fileStream = file.openRead(); - final assetRawUploadData = MultipartFile( - "assetData", - fileStream, - file.lengthSync(), - filename: originalFileName, - ); - - final baseRequest = ProgressMultipartRequest( - 'POST', - Uri.parse('$savedEndpoint/assets'), - abortTrigger: cancelToken.future, - onProgress: ((bytes, totalBytes) => onProgress(bytes, totalBytes)), - ); - - baseRequest.fields['deviceAssetId'] = asset.localId!; - baseRequest.fields['deviceId'] = deviceId; - baseRequest.fields['fileCreatedAt'] = asset.fileCreatedAt.toUtc().toIso8601String(); - baseRequest.fields['fileModifiedAt'] = asset.fileModifiedAt.toUtc().toIso8601String(); - baseRequest.fields['isFavorite'] = asset.isFavorite.toString(); - baseRequest.fields['duration'] = asset.duration.toString(); - baseRequest.files.add(assetRawUploadData); - - onCurrentAsset( - CurrentUploadAsset( - id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt.year == 1970 ? asset.fileModifiedAt : asset.fileCreatedAt, - fileName: originalFileName, - fileType: _getAssetType(asset.type), - fileSize: file.lengthSync(), - iCloudAsset: false, - ), - ); - - String? livePhotoVideoId; - if (asset.local!.isLivePhoto && livePhotoFile != null) { - livePhotoVideoId = await uploadLivePhotoVideo(originalFileName, livePhotoFile, baseRequest, cancelToken); - } - - if (livePhotoVideoId != null) { - baseRequest.fields['livePhotoVideoId'] = livePhotoVideoId; - } - - final response = await NetworkRepository.client.send(baseRequest); - - final responseBody = jsonDecode(await response.stream.bytesToString()); - - if (![200, 201].contains(response.statusCode)) { - final error = responseBody; - final errorMessage = error['message'] ?? error['error']; - - dPrint( - () => - "Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}", - ); - - onError( - ErrorUploadAsset( - asset: asset, - id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt, - fileName: originalFileName, - fileType: _getAssetType(candidate.asset.type), - errorMessage: errorMessage, - ), - ); - - if (errorMessage == "Quota has been exceeded!") { - anyErrors = true; - break; - } - - continue; - } - - bool isDuplicate = false; - if (response.statusCode == 200) { - isDuplicate = true; - duplicatedAssetIds.add(asset.localId!); - } - - onSuccess( - SuccessUploadAsset( - candidate: candidate, - remoteAssetId: responseBody['id'] as String, - isDuplicate: isDuplicate, - ), - ); - - if (shouldSyncAlbums) { - await _albumService.syncUploadAlbums(candidate.albumNames, [responseBody['id'] as String]); - } - } - } on RequestAbortedException { - dPrint(() => "Backup was cancelled by the user"); - anyErrors = true; - break; - } catch (error, stackTrace) { - dPrint(() => "Error backup asset: ${error.toString()}: $stackTrace"); - anyErrors = true; - continue; - } finally { - if (Platform.isIOS) { - try { - await file?.delete(); - await livePhotoFile?.delete(); - } catch (e) { - dPrint(() => "ERROR deleting file: ${e.toString()}"); - } - } - } - } - - if (duplicatedAssetIds.isNotEmpty) { - await _saveDuplicatedAssetIds(duplicatedAssetIds); - } - - return !anyErrors; - } - - Future uploadLivePhotoVideo( - String originalFileName, - File? livePhotoVideoFile, - MultipartRequest baseRequest, - Completer cancelToken, - ) async { - if (livePhotoVideoFile == null) { - return null; - } - final livePhotoTitle = p.setExtension(originalFileName, p.extension(livePhotoVideoFile.path)); - final fileStream = livePhotoVideoFile.openRead(); - final livePhotoRawUploadData = MultipartFile( - "assetData", - fileStream, - livePhotoVideoFile.lengthSync(), - filename: livePhotoTitle, - ); - final livePhotoReq = ProgressMultipartRequest(baseRequest.method, baseRequest.url, abortTrigger: cancelToken.future) - ..headers.addAll(baseRequest.headers) - ..fields.addAll(baseRequest.fields); - - livePhotoReq.files.add(livePhotoRawUploadData); - - var response = await NetworkRepository.client.send(livePhotoReq); - - var responseBody = jsonDecode(await response.stream.bytesToString()); - - if (![200, 201].contains(response.statusCode)) { - var error = responseBody; - - dPrint( - () => "Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}", - ); - } - - return responseBody.containsKey('id') ? responseBody['id'] : null; - } - - String _getAssetType(AssetType assetType) => switch (assetType) { - AssetType.audio => "AUDIO", - AssetType.image => "IMAGE", - AssetType.video => "VIDEO", - AssetType.other => "OTHER", - }; -} diff --git a/mobile/lib/services/backup_album.service.dart b/mobile/lib/services/backup_album.service.dart deleted file mode 100644 index ef9d1031de..0000000000 --- a/mobile/lib/services/backup_album.service.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; - -final backupAlbumServiceProvider = Provider((ref) { - return BackupAlbumService(ref.watch(backupAlbumRepositoryProvider)); -}); - -class BackupAlbumService { - final BackupAlbumRepository _backupAlbumRepository; - - const BackupAlbumService(this._backupAlbumRepository); - - Future> getAll({BackupAlbumSort? sort}) { - return _backupAlbumRepository.getAll(sort: sort); - } - - Future> getIdsBySelection(BackupSelection backup) { - return _backupAlbumRepository.getIdsBySelection(backup); - } - - Future> getAllBySelection(BackupSelection backup) { - return _backupAlbumRepository.getAllBySelection(backup); - } - - Future deleteAll(List ids) { - return _backupAlbumRepository.deleteAll(ids); - } - - Future updateAll(List backupAlbums) { - return _backupAlbumRepository.updateAll(backupAlbums); - } -} diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart deleted file mode 100644 index 2efd52cc81..0000000000 --- a/mobile/lib/services/backup_verification.service.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/utils/bootstrap.dart'; -import 'package:immich_mobile/utils/diff.dart'; - -/// Finds duplicates originating from missing EXIF information -class BackupVerificationService { - final UserService _userService; - final FileMediaRepository _fileMediaRepository; - final AssetRepository _assetRepository; - final IsarExifRepository _exifInfoRepository; - - const BackupVerificationService( - this._userService, - this._fileMediaRepository, - this._assetRepository, - this._exifInfoRepository, - ); - - /// Returns at most [limit] assets that were backed up without exif - Future> findWronglyBackedUpAssets({int limit = 100}) async { - final owner = _userService.getMyUser().id; - final List onlyLocal = await _assetRepository.getAll(ownerId: owner, state: AssetState.local, limit: limit); - final List remoteMatches = await _assetRepository.getMatches( - assets: onlyLocal, - ownerId: owner, - state: AssetState.remote, - limit: limit, - ); - final List localMatches = await _assetRepository.getMatches( - assets: remoteMatches, - ownerId: owner, - state: AssetState.local, - limit: limit, - ); - - final List deleteCandidates = [], originals = []; - - await diffSortedLists( - remoteMatches, - localMatches, - compare: (a, b) => a.fileName.compareTo(b.fileName), - both: (a, b) async { - a.exifInfo = await _exifInfoRepository.get(a.id); - deleteCandidates.add(a); - originals.add(b); - return false; - }, - onlyFirst: (a) {}, - onlySecond: (b) {}, - ); - final isolateToken = ServicesBinding.rootIsolateToken!; - final List toDelete; - if (deleteCandidates.length > 10) { - // performs 2 checks in parallel for a nice speedup - final half = deleteCandidates.length ~/ 2; - final lower = compute(_computeSaveToDelete, ( - deleteCandidates: deleteCandidates.slice(0, half), - originals: originals.slice(0, half), - endpoint: Store.get(StoreKey.serverEndpoint), - rootIsolateToken: isolateToken, - fileMediaRepository: _fileMediaRepository, - )); - final upper = compute(_computeSaveToDelete, ( - deleteCandidates: deleteCandidates.slice(half), - originals: originals.slice(half), - endpoint: Store.get(StoreKey.serverEndpoint), - rootIsolateToken: isolateToken, - fileMediaRepository: _fileMediaRepository, - )); - toDelete = await lower + await upper; - } else { - toDelete = await compute(_computeSaveToDelete, ( - deleteCandidates: deleteCandidates, - originals: originals, - endpoint: Store.get(StoreKey.serverEndpoint), - rootIsolateToken: isolateToken, - fileMediaRepository: _fileMediaRepository, - )); - } - return toDelete; - } - - static Future> _computeSaveToDelete( - ({ - List deleteCandidates, - List originals, - String endpoint, - RootIsolateToken rootIsolateToken, - FileMediaRepository fileMediaRepository, - }) - tuple, - ) async { - assert(tuple.deleteCandidates.length == tuple.originals.length); - final List result = []; - BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); - await tuple.fileMediaRepository.enableBackgroundAccess(); - final ApiService apiService = ApiService(); - apiService.setEndpoint(tuple.endpoint); - for (int i = 0; i < tuple.deleteCandidates.length; i++) { - if (await _compareAssets(tuple.deleteCandidates[i], tuple.originals[i], apiService)) { - result.add(tuple.deleteCandidates[i]); - } - } - return result; - } - - static Future _compareAssets(Asset remote, Asset local, ApiService apiService) async { - if (remote.checksum == local.checksum) return false; - ExifInfo? exif = remote.exifInfo; - if (exif != null && exif.latitude != null) return false; - if (exif == null || exif.fileSize == null) { - final dto = await apiService.assetsApi.getAssetInfo(remote.remoteId!); - if (dto != null && dto.exifInfo != null) { - exif = ExifDtoConverter.fromDto(dto.exifInfo!); - } - } - final file = await local.local!.originFile; - if (exif != null && file != null && exif.fileSize != null) { - final origSize = await file.length(); - if (exif.fileSize! == origSize || exif.fileSize! != origSize) { - final latLng = await local.local!.latlngAsync(); - - if (exif.latitude == null && - latLng.latitude != null && - (remote.fileCreatedAt.isAtSameMomentAs(local.fileCreatedAt) || - remote.fileModifiedAt.isAtSameMomentAs(local.fileModifiedAt) || - _sameExceptTimeZone(remote.fileCreatedAt, local.fileCreatedAt))) { - if (remote.type == AssetType.video) { - // it's very unlikely that a video of same length, filesize, name - // and date is wrong match. Cannot easily compare videos anyway - return true; - } - - // for images: make sure they are pixel-wise identical - // (skip first few KBs containing metadata) - final Uint64List localImage = _fakeDecodeImg(await file.readAsBytes()); - final res = await apiService.assetsApi.downloadAssetWithHttpInfo(remote.remoteId!); - final Uint64List remoteImage = _fakeDecodeImg(res.bodyBytes); - - final eq = const ListEquality().equals(remoteImage, localImage); - return eq; - } - } - } - - return false; - } - - static Uint64List _fakeDecodeImg(Uint8List bytes) { - const headerLength = 131072; // assume header is at most 128 KB - final start = bytes.length < headerLength * 2 ? (bytes.length ~/ (4 * 8)) * 8 : headerLength; - return bytes.buffer.asUint64List(start); - } - - static bool _sameExceptTimeZone(DateTime a, DateTime b) { - final ms = a.isAfter(b) - ? a.millisecondsSinceEpoch - b.millisecondsSinceEpoch - : b.millisecondsSinceEpoch - a.microsecondsSinceEpoch; - final x = ms / (1000 * 60 * 30); - final y = ms ~/ (1000 * 60 * 30); - return y.toDouble() == x && y < 24; - } -} - -final backupVerificationServiceProvider = Provider( - (ref) => BackupVerificationService( - ref.watch(userServiceProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(exifRepositoryProvider), - ), -); diff --git a/mobile/lib/services/deep_link.service.dart b/mobile/lib/services/deep_link.service.dart index 9d2bdbe4a0..5ff0fa8a4d 100644 --- a/mobile/lib/services/deep_link.service.dart +++ b/mobile/lib/services/deep_link.service.dart @@ -7,10 +7,7 @@ import 'package:immich_mobile/domain/services/memory.service.dart'; import 'package:immich_mobile/domain/services/people.service.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart' as beta_asset_provider; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; @@ -18,19 +15,9 @@ import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/services/memory.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; final deepLinkServiceProvider = Provider( (ref) => DeepLinkService( - ref.watch(memoryServiceProvider), - ref.watch(assetServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(currentAssetProvider.notifier), - ref.watch(currentAlbumProvider.notifier), - // Below is used for beta timeline ref.watch(timelineFactoryProvider), ref.watch(beta_asset_provider.assetServiceProvider), ref.watch(remoteAlbumServiceProvider), @@ -41,14 +28,6 @@ final deepLinkServiceProvider = Provider( ); class DeepLinkService { - /// TODO: Remove this when beta is default - final MemoryService _memoryService; - final AssetService _assetService; - final AlbumService _albumService; - final CurrentAsset _currentAsset; - final CurrentAlbum _currentAlbum; - - /// Used for beta timeline final TimelineFactory _betaTimelineFactory; final beta_asset_service.AssetService _betaAssetService; final RemoteAlbumService _betaRemoteAlbumService; @@ -58,11 +37,6 @@ class DeepLinkService { final UserDto? _currentUser; const DeepLinkService( - this._memoryService, - this._assetService, - this._albumService, - this._currentAsset, - this._currentAlbum, this._betaTimelineFactory, this._betaAssetService, this._betaRemoteAlbumService, @@ -75,7 +49,7 @@ class DeepLinkService { return DeepLink([ // we need something to segue back to if the app was cold started // TODO: use MainTimelineRoute this when beta is default - if (isColdStart) (Store.isBetaTimelineEnabled) ? const TabShellRoute() : const PhotosRoute(), + if (isColdStart) const TabShellRoute(), route, ]); } @@ -138,95 +112,52 @@ class DeepLinkService { } Future _buildMemoryDeepLink(String? memoryId) async { - if (Store.isBetaTimelineEnabled) { - List memories = []; + List memories = []; - if (memoryId == null) { - if (_currentUser == null) { - return null; - } - - memories = await _betaMemoryService.getMemoryLane(_currentUser.id); - } else { - final memory = await _betaMemoryService.get(memoryId); - if (memory != null) { - memories = [memory]; - } - } - - if (memories.isEmpty) { + if (memoryId == null) { + if (_currentUser == null) { return null; } - return DriftMemoryRoute(memories: memories, memoryIndex: 0); + memories = await _betaMemoryService.getMemoryLane(_currentUser.id); } else { - // TODO: Remove this when beta is default - if (memoryId == null) { - return null; + final memory = await _betaMemoryService.get(memoryId); + if (memory != null) { + memories = [memory]; } - final memory = await _memoryService.getMemoryById(memoryId); - - if (memory == null) { - return null; - } - - return MemoryRoute(memories: [memory], memoryIndex: 0); } - } - Future _buildAssetDeepLink(String assetId, WidgetRef ref) async { - if (Store.isBetaTimelineEnabled) { - final asset = await _betaAssetService.getRemoteAsset(assetId); - if (asset == null) { - return null; - } - - AssetViewer.setAsset(ref, asset); - return AssetViewerRoute( - initialIndex: 0, - timelineService: _betaTimelineFactory.fromAssets([asset], TimelineOrigin.deepLink), - ); - } else { - // TODO: Remove this when beta is default - final asset = await _assetService.getAssetByRemoteId(assetId); - if (asset == null) { - return null; - } - - _currentAsset.set(asset); - final renderList = await RenderList.fromAssets([asset], GroupAssetsBy.auto); - - return GalleryViewerRoute(renderList: renderList, initialIndex: 0, heroOffset: 0, showStack: true); - } - } - - Future _buildAlbumDeepLink(String albumId) async { - if (Store.isBetaTimelineEnabled) { - final album = await _betaRemoteAlbumService.get(albumId); - - if (album == null) { - return null; - } - - return RemoteAlbumRoute(album: album); - } else { - // TODO: Remove this when beta is default - final album = await _albumService.getAlbumByRemoteId(albumId); - - if (album == null) { - return null; - } - - _currentAlbum.set(album); - return AlbumViewerRoute(albumId: album.id); - } - } - - Future _buildActivityDeepLink(String albumId) async { - if (Store.isBetaTimelineEnabled == false) { + if (memories.isEmpty) { return null; } + return DriftMemoryRoute(memories: memories, memoryIndex: 0); + } + + Future _buildAssetDeepLink(String assetId, WidgetRef ref) async { + final asset = await _betaAssetService.getRemoteAsset(assetId); + if (asset == null) { + return null; + } + + AssetViewer.setAsset(ref, asset); + return AssetViewerRoute( + initialIndex: 0, + timelineService: _betaTimelineFactory.fromAssets([asset], TimelineOrigin.deepLink), + ); + } + + Future _buildAlbumDeepLink(String albumId) async { + final album = await _betaRemoteAlbumService.get(albumId); + + if (album == null) { + return null; + } + + return RemoteAlbumRoute(album: album); + } + + Future _buildActivityDeepLink(String albumId) async { final album = await _betaRemoteAlbumService.get(albumId); if (album == null || album.isActivityEnabled == false) { @@ -237,10 +168,6 @@ class DeepLinkService { } Future _buildPeopleDeepLink(String personId) async { - if (Store.isBetaTimelineEnabled == false) { - return null; - } - final person = await _betaPeopleService.get(personId); if (person == null) { diff --git a/mobile/lib/services/device.service.dart b/mobile/lib/services/device.service.dart deleted file mode 100644 index 50a0d93b24..0000000000 --- a/mobile/lib/services/device.service.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter_udid/flutter_udid.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'; - -final deviceServiceProvider = Provider((ref) => const DeviceService()); - -class DeviceService { - const DeviceService(); - - createDeviceId() { - return FlutterUdid.consistentUdid; - } - - /// Returns the device ID from local storage or creates a new one if not found. - /// - /// This method first attempts to retrieve the device ID from the local store using - /// [StoreKey.deviceId]. If no device ID is found (returns null), it generates a - /// new device ID by calling [createDeviceId]. - /// - /// Returns a [String] representing the device's unique identifier. - String getDeviceId() { - return Store.tryGet(StoreKey.deviceId) ?? createDeviceId(); - } -} diff --git a/mobile/lib/services/download.service.dart b/mobile/lib/services/download.service.dart index 7d2cf01b7c..3f2c36fa7e 100644 --- a/mobile/lib/services/download.service.dart +++ b/mobile/lib/services/download.service.dart @@ -3,14 +3,9 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/services.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/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/repositories/download.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; final downloadServiceProvider = Provider( @@ -54,7 +49,7 @@ class DownloadService { final title = task.filename; final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; try { - final Asset? resultAsset = await _fileMediaRepository.saveImageWithFile( + final resultAsset = await _fileMediaRepository.saveImageWithFile( filePath, title: title, relativePath: relativePath, @@ -76,7 +71,7 @@ class DownloadService { final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; final file = File(filePath); try { - final Asset? resultAsset = await _fileMediaRepository.saveVideo(file, title: title, relativePath: relativePath); + final resultAsset = await _fileMediaRepository.saveVideo(file, title: title, relativePath: relativePath); return resultAsset != null; } catch (error, stack) { _log.severe("Error saving video", error, stack); @@ -109,7 +104,7 @@ class DownloadService { return result != null; } on PlatformException catch (error, stack) { // Handle saving MotionPhotos on iOS - if (error.code == 'PHPhotosErrorDomain (-1)') { + if (error.code.startsWith('PHPhotosErrorDomain')) { final result = await _fileMediaRepository.saveImageWithFile(imageFilePath, title: task.filename); return result != null; } @@ -136,62 +131,6 @@ class DownloadService { Future cancelDownload(String id) async { return await FileDownloader().cancelTaskWithId(id); } - - Future> downloadAll(List assets) async { - return await _downloadRepository.downloadAll(assets.expand(_createDownloadTasks).toList()); - } - - Future download(Asset asset) async { - final tasks = _createDownloadTasks(asset); - await _downloadRepository.downloadAll(tasks); - } - - List _createDownloadTasks(Asset asset) { - if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) { - return [ - _buildDownloadTask( - asset.remoteId!, - asset.fileName, - group: kDownloadGroupLivePhoto, - metadata: LivePhotosMetadata(part: LivePhotosPart.image, id: asset.remoteId!).toJson(), - ), - _buildDownloadTask( - asset.livePhotoVideoId!, - asset.fileName.toUpperCase().replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'), - group: kDownloadGroupLivePhoto, - metadata: LivePhotosMetadata(part: LivePhotosPart.video, id: asset.remoteId!).toJson(), - ), - ]; - } - - if (asset.remoteId == null) { - return []; - } - - return [ - _buildDownloadTask( - asset.remoteId!, - asset.fileName, - group: asset.isImage ? kDownloadGroupImage : kDownloadGroupVideo, - ), - ]; - } - - DownloadTask _buildDownloadTask(String id, String filename, {String? group, String? metadata}) { - final path = r'/assets/{id}/original'.replaceAll('{id}', id); - final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final headers = ApiService.getRequestHeaders(); - - return DownloadTask( - taskId: id, - url: serverEndpoint + path, - headers: headers, - filename: filename, - updates: Updates.statusAndProgress, - group: group ?? '', - metaData: metadata ?? '', - ); - } } TaskRecord _findTaskRecord(List records, String livePhotosId, LivePhotosPart part) { diff --git a/mobile/lib/services/entity.service.dart b/mobile/lib/services/entity.service.dart deleted file mode 100644 index fe7358fce6..0000000000 --- a/mobile/lib/services/entity.service.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; - -class EntityService { - final AssetRepository _assetRepository; - final IsarUserRepository _isarUserRepository; - const EntityService(this._assetRepository, this._isarUserRepository); - - Future fillAlbumWithDatabaseEntities(Album album) async { - final ownerId = album.ownerId; - if (ownerId != null) { - // replace owner with user from database - final user = await _isarUserRepository.getByUserId(ownerId); - album.owner.value = user == null ? null : User.fromDto(user); - } - final thumbnailAssetId = album.remoteThumbnailAssetId ?? album.thumbnail.value?.remoteId; - if (thumbnailAssetId != null) { - // set thumbnail with asset from database - album.thumbnail.value = await _assetRepository.getByRemoteId(thumbnailAssetId); - } - if (album.remoteUsers.isNotEmpty) { - // replace all users with users from database - final users = await _isarUserRepository.getByUserIds(album.remoteUsers.map((user) => user.id).toList()); - album.sharedUsers.clear(); - album.sharedUsers.addAll(users.nonNulls.map(User.fromDto)); - album.shared = true; - } - if (album.remoteAssets.isNotEmpty) { - // replace all assets with assets from database - final assets = await _assetRepository.getAllByRemoteId(album.remoteAssets.map((asset) => asset.remoteId!)); - album.assets.clear(); - album.assets.addAll(assets); - } - return album; - } -} - -final entityServiceProvider = Provider( - (ref) => EntityService(ref.watch(assetRepositoryProvider), ref.watch(userRepositoryProvider)), -); diff --git a/mobile/lib/services/etag.service.dart b/mobile/lib/services/etag.service.dart deleted file mode 100644 index 00eb83fcea..0000000000 --- a/mobile/lib/services/etag.service.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; - -final etagServiceProvider = Provider((ref) => ETagService(ref.watch(etagRepositoryProvider))); - -class ETagService { - final ETagRepository _eTagRepository; - - const ETagService(this._eTagRepository); - - Future clearTable() { - return _eTagRepository.clearTable(); - } -} diff --git a/mobile/lib/services/exif.service.dart b/mobile/lib/services/exif.service.dart deleted file mode 100644 index 57f793b21e..0000000000 --- a/mobile/lib/services/exif.service.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; - -final exifServiceProvider = Provider((ref) => ExifService(ref.watch(exifRepositoryProvider))); - -class ExifService { - final IsarExifRepository _exifInfoRepository; - - const ExifService(this._exifInfoRepository); - - Future clearTable() { - return _exifInfoRepository.deleteAll(); - } -} diff --git a/mobile/lib/services/folder.service.dart b/mobile/lib/services/folder.service.dart index 91fb455110..bf7590ce54 100644 --- a/mobile/lib/services/folder.service.dart +++ b/mobile/lib/services/folder.service.dart @@ -1,6 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; import 'package:immich_mobile/models/folder/root_folder.model.dart'; import 'package:immich_mobile/repositories/folder_api.repository.dart'; @@ -76,7 +76,7 @@ class FolderService { return RootFolder(subfolders: rootSubfolders, path: '/'); } - Future> getFolderAssets(RootFolder folder, SortOrder order) async { + Future> getFolderAssets(RootFolder folder, SortOrder order) async { try { if (folder is RecursiveFolder) { String fullPath = folder.path.isEmpty ? folder.name : '${folder.path}/${folder.name}'; @@ -84,9 +84,9 @@ class FolderService { var result = await _folderApiRepository.getAssetsForPath(fullPath); if (order == SortOrder.desc) { - result.sort((a, b) => b.fileCreatedAt.compareTo(a.fileCreatedAt)); + result.sort((a, b) => b.createdAt.compareTo(a.createdAt)); } else { - result.sort((a, b) => a.fileCreatedAt.compareTo(b.fileCreatedAt)); + result.sort((a, b) => a.createdAt.compareTo(b.createdAt)); } return result; diff --git a/mobile/lib/services/hash.service.dart b/mobile/lib/services/hash.service.dart deleted file mode 100644 index 9d1f4e51e8..0000000000 --- a/mobile/lib/services/hash.service.dart +++ /dev/null @@ -1,191 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/device_asset.provider.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:logging/logging.dart'; - -class HashService { - HashService({ - required IsarDeviceAssetRepository deviceAssetRepository, - required BackgroundService backgroundService, - this.batchSizeLimit = kBatchHashSizeLimit, - int? batchFileLimit, - }) : _deviceAssetRepository = deviceAssetRepository, - _backgroundService = backgroundService, - batchFileLimit = batchFileLimit ?? kBatchHashFileLimit; - - final IsarDeviceAssetRepository _deviceAssetRepository; - final BackgroundService _backgroundService; - final int batchSizeLimit; - final int batchFileLimit; - final _log = Logger('HashService'); - - /// Processes a list of local [Asset]s, storing their hash and returning only those - /// that were successfully hashed. Hashes are looked up in a DB table - /// [DeviceAsset] by local id. Only missing entries are newly hashed and added to the DB table. - Future> hashAssets(List assets) async { - assets.sort(Asset.compareByLocalId); - - // Get and sort DB entries - guaranteed to be a subset of assets - final hashesInDB = await _deviceAssetRepository.getByIds(assets.map((a) => a.localId!).toList()); - hashesInDB.sort((a, b) => a.assetId.compareTo(b.assetId)); - - int dbIndex = 0; - int bytesProcessed = 0; - final hashedAssets = []; - final toBeHashed = <_AssetPath>[]; - final toBeDeleted = []; - - for (int assetIndex = 0; assetIndex < assets.length; assetIndex++) { - final asset = assets[assetIndex]; - DeviceAsset? matchingDbEntry; - - if (dbIndex < hashesInDB.length) { - final deviceAsset = hashesInDB[dbIndex]; - if (deviceAsset.assetId == asset.localId) { - matchingDbEntry = deviceAsset; - dbIndex++; - } - } - - if (matchingDbEntry != null && - matchingDbEntry.hash.isNotEmpty && - matchingDbEntry.modifiedTime.isAtSameMomentAs(asset.fileModifiedAt)) { - // Reuse the existing hash - hashedAssets.add(asset.copyWith(checksum: base64.encode(matchingDbEntry.hash))); - continue; - } - - final file = await _tryGetAssetFile(asset); - if (file == null) { - // Can't access file, delete any DB entry - if (matchingDbEntry != null) { - toBeDeleted.add(matchingDbEntry.assetId); - } - continue; - } - - bytesProcessed += await file.length(); - toBeHashed.add(_AssetPath(asset: asset, path: file.path)); - - if (_shouldProcessBatch(toBeHashed.length, bytesProcessed)) { - hashedAssets.addAll(await _processBatch(toBeHashed, toBeDeleted)); - toBeHashed.clear(); - toBeDeleted.clear(); - bytesProcessed = 0; - } - } - assert(dbIndex == hashesInDB.length, "All hashes should've been processed"); - - // Process any remaining files - if (toBeHashed.isNotEmpty) { - hashedAssets.addAll(await _processBatch(toBeHashed, toBeDeleted)); - } - - // Clean up deleted references - if (toBeDeleted.isNotEmpty) { - await _deviceAssetRepository.deleteIds(toBeDeleted); - } - - return hashedAssets; - } - - bool _shouldProcessBatch(int assetCount, int bytesProcessed) => - assetCount >= batchFileLimit || bytesProcessed >= batchSizeLimit; - - Future _tryGetAssetFile(Asset asset) async { - try { - final file = await asset.local!.originFile; - if (file == null) { - _log.warning( - "Failed to get file for asset ${asset.localId ?? ''}, name: ${asset.fileName}, created on: ${asset.fileCreatedAt}, skipping", - ); - return null; - } - return file; - } catch (error, stackTrace) { - _log.warning( - "Error getting file to hash for asset ${asset.localId ?? ''}, name: ${asset.fileName}, created on: ${asset.fileCreatedAt}, skipping", - error, - stackTrace, - ); - return null; - } - } - - /// Processes a batch of files and returns a list of successfully hashed assets after saving - /// them in [DeviceAssetToHash] for future retrieval - Future> _processBatch(List<_AssetPath> toBeHashed, List toBeDeleted) async { - _log.info("Hashing ${toBeHashed.length} files"); - final hashes = await _hashFiles(toBeHashed.map((e) => e.path).toList()); - assert( - hashes.length == toBeHashed.length, - "Number of Hashes returned from platform should be the same as the input", - ); - - final hashedAssets = []; - final toBeAdded = []; - - for (final (index, hash) in hashes.indexed) { - final asset = toBeHashed.elementAtOrNull(index)?.asset; - if (asset != null && hash?.length == 20) { - hashedAssets.add(asset.copyWith(checksum: base64.encode(hash!))); - toBeAdded.add(DeviceAsset(assetId: asset.localId!, hash: hash, modifiedTime: asset.fileModifiedAt)); - } else { - _log.warning("Failed to hash file ${asset?.localId ?? ''}"); - if (asset != null) { - toBeDeleted.add(asset.localId!); - } - } - } - - // Update the DB for future retrieval - await _deviceAssetRepository.transaction(() async { - await _deviceAssetRepository.updateAll(toBeAdded); - await _deviceAssetRepository.deleteIds(toBeDeleted); - }); - - _log.fine("Hashed ${hashedAssets.length}/${toBeHashed.length} assets"); - return hashedAssets; - } - - /// Hashes the given files and returns a list of the same length. - /// Files that could not be hashed will have a `null` value - Future> _hashFiles(List paths) async { - try { - final hashes = await _backgroundService.digestFiles(paths); - if (hashes != null) { - return hashes; - } - _log.severe("Hashing ${paths.length} files failed"); - } catch (e, s) { - _log.severe("Error occurred while hashing assets", e, s); - } - return List.filled(paths.length, null); - } -} - -class _AssetPath { - final Asset asset; - final String path; - - const _AssetPath({required this.asset, required this.path}); - - _AssetPath copyWith({Asset? asset, String? path}) { - return _AssetPath(asset: asset ?? this.asset, path: path ?? this.path); - } -} - -final hashServiceProvider = Provider( - (ref) => HashService( - deviceAssetRepository: ref.watch(deviceAssetRepositoryProvider), - backgroundService: ref.watch(backgroundServiceProvider), - ), -); diff --git a/mobile/lib/services/local_notification.service.dart b/mobile/lib/services/local_notification.service.dart deleted file mode 100644 index bf85f4a9a9..0000000000 --- a/mobile/lib/services/local_notification.service.dart +++ /dev/null @@ -1,118 +0,0 @@ -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), -); - -class LocalNotificationService { - final FlutterLocalNotificationsPlugin _localNotificationPlugin = FlutterLocalNotificationsPlugin(); - final PermissionStatus _permissionStatus; - final Ref ref; - - LocalNotificationService(this._permissionStatus, this.ref); - - static const manualUploadNotificationID = 4; - static const manualUploadDetailedNotificationID = 5; - static const manualUploadChannelName = 'Manual Asset Upload'; - static const manualUploadChannelID = 'immich/manualUpload'; - static const manualUploadChannelNameDetailed = 'Manual Asset Upload Detailed'; - static const manualUploadDetailedChannelID = 'immich/manualUploadDetailed'; - static const cancelUploadActionID = 'cancel_upload'; - - Future setup() async { - const androidSetting = AndroidInitializationSettings('@drawable/notification_icon'); - const iosSetting = DarwinInitializationSettings(); - - const initSettings = InitializationSettings(android: androidSetting, iOS: iosSetting); - - await _localNotificationPlugin.initialize( - initSettings, - onDidReceiveNotificationResponse: _onDidReceiveForegroundNotificationResponse, - ); - } - - Future _showOrUpdateNotification( - int id, - String title, - String body, - AndroidNotificationDetails androidNotificationDetails, - DarwinNotificationDetails iosNotificationDetails, - ) async { - final notificationDetails = NotificationDetails(android: androidNotificationDetails, iOS: iosNotificationDetails); - - if (_permissionStatus == PermissionStatus.granted) { - await _localNotificationPlugin.show(id, title, body, notificationDetails); - } - } - - Future closeNotification(int id) { - return _localNotificationPlugin.cancel(id); - } - - Future showOrUpdateManualUploadStatus( - String title, - String body, { - bool? isDetailed, - bool? presentBanner, - bool? showActions, - int? maxProgress, - int? progress, - }) { - var notificationlId = manualUploadNotificationID; - var androidChannelID = manualUploadChannelID; - var androidChannelName = manualUploadChannelName; - // Separate Notification for Info/Alerts and Progress - if (isDetailed != null && isDetailed) { - notificationlId = manualUploadDetailedNotificationID; - androidChannelID = manualUploadDetailedChannelID; - androidChannelName = manualUploadChannelNameDetailed; - } - // Progress notification - final androidNotificationDetails = (maxProgress != null && progress != null) - ? AndroidNotificationDetails( - androidChannelID, - androidChannelName, - ticker: title, - showProgress: true, - onlyAlertOnce: true, - maxProgress: maxProgress, - progress: progress, - indeterminate: false, - playSound: false, - priority: Priority.low, - importance: Importance.low, - ongoing: true, - actions: (showActions ?? false) - ? [ - const AndroidNotificationAction(cancelUploadActionID, 'Cancel', showsUserInterface: true), - ] - : null, - ) - // Non-progress notification - : AndroidNotificationDetails(androidChannelID, androidChannelName, playSound: false); - - final iosNotificationDetails = DarwinNotificationDetails( - presentBadge: true, - presentList: true, - presentBanner: presentBanner, - ); - - return _showOrUpdateNotification(notificationlId, title, body, androidNotificationDetails, iosNotificationDetails); - } - - void _onDidReceiveForegroundNotificationResponse(NotificationResponse notificationResponse) { - // Handle notification actions - switch (notificationResponse.actionId) { - case cancelUploadActionID: - { - dPrint(() => "User cancelled manual upload operation"); - ref.read(manualUploadProvider.notifier).cancelBackup(); - } - } - } -} diff --git a/mobile/lib/services/memory.service.dart b/mobile/lib/services/memory.service.dart deleted file mode 100644 index e485bb0957..0000000000 --- a/mobile/lib/services/memory.service.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:logging/logging.dart'; - -final memoryServiceProvider = StateProvider((ref) { - return MemoryService(ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider)); -}); - -class MemoryService { - final log = Logger("MemoryService"); - - final ApiService _apiService; - final AssetRepository _assetRepository; - - MemoryService(this._apiService, this._assetRepository); - - Future?> getMemoryLane() async { - try { - final now = DateTime.now(); - final data = await _apiService.memoriesApi.searchMemories( - for_: DateTime.utc(now.year, now.month, now.day, 0, 0, 0), - ); - - if (data == null) { - return null; - } - - List memories = []; - - for (final memory in data) { - final dbAssets = await _assetRepository.getAllByRemoteId(memory.assets.map((e) => e.id)); - final yearsAgo = now.year - memory.data.year; - if (dbAssets.isNotEmpty) { - final String title = 'years_ago'.t(args: {'years': yearsAgo.toString()}); - memories.add(Memory(title: title, assets: dbAssets)); - } - } - - return memories.isNotEmpty ? memories : null; - } catch (error, stack) { - log.severe("Cannot get memories", error, stack); - return null; - } - } - - Future getMemoryById(String id) async { - try { - final memoryResponse = await _apiService.memoriesApi.getMemory(id); - - if (memoryResponse == null) { - return null; - } - final dbAssets = await _assetRepository.getAllByRemoteId(memoryResponse.assets.map((e) => e.id)); - if (dbAssets.isEmpty) { - log.warning("No assets found for memory with ID: $id"); - return null; - } - final yearsAgo = DateTime.now().year - memoryResponse.data.year; - final String title = 'years_ago'.t(args: {'years': yearsAgo.toString()}); - - return Memory(title: title, assets: dbAssets); - } catch (error, stack) { - log.severe("Cannot get memory with ID: $id", error, stack); - return null; - } - } -} diff --git a/mobile/lib/services/partner.service.dart b/mobile/lib/services/partner.service.dart deleted file mode 100644 index b8e5ae9a4d..0000000000 --- a/mobile/lib/services/partner.service.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/partner.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:logging/logging.dart'; - -final partnerServiceProvider = Provider( - (ref) => PartnerService( - ref.watch(partnerApiRepositoryProvider), - ref.watch(userRepositoryProvider), - ref.watch(partnerRepositoryProvider), - ), -); - -class PartnerService { - final PartnerApiRepository _partnerApiRepository; - final PartnerRepository _partnerRepository; - final IsarUserRepository _isarUserRepository; - final Logger _log = Logger("PartnerService"); - - PartnerService(this._partnerApiRepository, this._isarUserRepository, this._partnerRepository); - - Future> getSharedWith() async { - return _partnerRepository.getSharedWith(); - } - - Future> getSharedBy() async { - return _partnerRepository.getSharedBy(); - } - - Stream> watchSharedWith() { - return _partnerRepository.watchSharedWith(); - } - - Stream> watchSharedBy() { - return _partnerRepository.watchSharedBy(); - } - - Future removePartner(UserDto partner) async { - try { - await _partnerApiRepository.delete(partner.id); - await _isarUserRepository.update(partner.copyWith(isPartnerSharedBy: false)); - } catch (e) { - _log.warning("Failed to remove partner ${partner.id}", e); - return false; - } - return true; - } - - Future addPartner(UserDto partner) async { - try { - await _partnerApiRepository.create(partner.id); - await _isarUserRepository.update(partner.copyWith(isPartnerSharedBy: true)); - return true; - } catch (e) { - _log.warning("Failed to add partner ${partner.id}", e); - } - return false; - } - - Future updatePartner(UserDto partner, {required bool inTimeline}) async { - try { - final dto = await _partnerApiRepository.update(partner.id, inTimeline: inTimeline); - await _isarUserRepository.update(partner.copyWith(inTimeline: dto.inTimeline)); - return true; - } catch (e) { - _log.warning("Failed to update partner ${partner.id}", e); - } - return false; - } -} diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart index 37b16a8d29..0d589ea71d 100644 --- a/mobile/lib/services/person.service.dart +++ b/mobile/lib/services/person.service.dart @@ -1,28 +1,16 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/person_api.repository.dart'; import 'package:logging/logging.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'person.service.g.dart'; - -@riverpod -PersonService personService(Ref ref) => PersonService( - ref.watch(personApiRepositoryProvider), - ref.watch(assetApiRepositoryProvider), - ref.read(assetRepositoryProvider), +final personServiceProvider = Provider.autoDispose( + (ref) => PersonService(ref.watch(personApiRepositoryProvider)), ); class PersonService { final Logger _log = Logger("PersonService"); final PersonApiRepository _personApiRepository; - final AssetApiRepository _assetApiRepository; - final AssetRepository _assetRepository; - - PersonService(this._personApiRepository, this._assetApiRepository, this._assetRepository); + PersonService(this._personApiRepository); Future> getAllPeople() async { try { @@ -33,16 +21,6 @@ class PersonService { } } - Future> getPersonAssets(String id) async { - try { - final assets = await _assetApiRepository.search(personIds: [id]); - return await _assetRepository.getAllByRemoteId(assets.map((a) => a.remoteId!)); - } catch (error, stack) { - _log.severe("Error while fetching person assets", error, stack); - } - return []; - } - Future updateName(String id, String name) async { try { return await _personApiRepository.update(id, name: name); diff --git a/mobile/lib/services/person.service.g.dart b/mobile/lib/services/person.service.g.dart deleted file mode 100644 index 8c2d46b3bd..0000000000 --- a/mobile/lib/services/person.service.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'person.service.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$personServiceHash() => r'10883bccc6c402205e6785cf9ee6cd7142cd0983'; - -/// See also [personService]. -@ProviderFor(personService) -final personServiceProvider = AutoDisposeProvider.internal( - personService, - name: r'personServiceProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$personServiceHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef PersonServiceRef = AutoDisposeProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index f33adf80f9..0330c8485c 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -1,31 +1,22 @@ 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'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/models/search/search_result.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/search.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; final searchServiceProvider = Provider( - (ref) => SearchService( - ref.watch(apiServiceProvider), - ref.watch(assetRepositoryProvider), - ref.watch(searchApiRepositoryProvider), - ), + (ref) => SearchService(ref.watch(apiServiceProvider), ref.watch(searchApiRepositoryProvider)), ); class SearchService { final ApiService _apiService; - final AssetRepository _assetRepository; final SearchApiRepository _searchApiRepository; final _log = Logger("SearchService"); - SearchService(this._apiService, this._assetRepository, this._searchApiRepository); + SearchService(this._apiService, this._searchApiRepository); Future?> getSearchSuggestions( SearchSuggestionType type, { @@ -48,24 +39,6 @@ class SearchService { } } - Future search(SearchFilter filter, int page) async { - try { - final response = await _searchApiRepository.search(filter, page); - - if (response == null || response.assets.items.isEmpty) { - return null; - } - - return SearchResult( - assets: await _assetRepository.getAllByRemoteId(response.assets.items.map((e) => e.id)), - nextPage: response.assets.nextPage?.toInt(), - ); - } catch (error, stackTrace) { - _log.severe("Failed to search for assets", error, stackTrace); - } - return null; - } - Future?> getExploreData() async { try { return await _apiService.searchApi.getExploreData(); diff --git a/mobile/lib/services/share.service.dart b/mobile/lib/services/share.service.dart deleted file mode 100644 index a0998d6d3d..0000000000 --- a/mobile/lib/services/share.service.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/response_extensions.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:logging/logging.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:share_plus/share_plus.dart'; - -final shareServiceProvider = Provider((ref) => ShareService(ref.watch(apiServiceProvider))); - -class ShareService { - final ApiService _apiService; - final Logger _log = Logger("ShareService"); - - ShareService(this._apiService); - - Future shareAsset(Asset asset, BuildContext context) async { - return await shareAssets([asset], context); - } - - Future shareAssets(List assets, BuildContext context) async { - try { - final downloadedXFiles = []; - - for (var asset in assets) { - if (asset.isLocal) { - // Prefer local assets to share - File? f = await asset.local!.originFile; - downloadedXFiles.add(XFile(f!.path)); - } else if (asset.isRemote) { - // Download remote asset otherwise - final tempDir = await getTemporaryDirectory(); - final fileName = asset.fileName; - final tempFile = await File('${tempDir.path}/$fileName').create(); - final res = await _apiService.assetsApi.downloadAssetWithHttpInfo(asset.remoteId!); - - if (res.statusCode != 200) { - _log.severe("Asset download for ${asset.fileName} failed", res.toLoggerString()); - continue; - } - - tempFile.writeAsBytesSync(res.bodyBytes); - downloadedXFiles.add(XFile(tempFile.path)); - } - } - - if (downloadedXFiles.isEmpty) { - _log.warning("No asset can be retrieved for share"); - return false; - } - - if (downloadedXFiles.length != assets.length) { - _log.warning("Partial share - Requested: ${assets.length}, Sharing: ${downloadedXFiles.length}"); - } - - final size = MediaQuery.of(context).size; - unawaited( - Share.shareXFiles( - downloadedXFiles, - sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)), - ), - ); - return true; - } catch (error) { - _log.severe("Share failed", error); - } - return false; - } -} diff --git a/mobile/lib/services/stack.service.dart b/mobile/lib/services/stack.service.dart deleted file mode 100644 index 88189c6bcd..0000000000 --- a/mobile/lib/services/stack.service.dart +++ /dev/null @@ -1,64 +0,0 @@ -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); - - final ApiService _api; - final AssetRepository _assetRepository; - - Future getStack(String stackId) async { - try { - return _api.stacksApi.getStack(stackId); - } catch (error) { - dPrint(() => "Error while fetching stack: $error"); - } - return null; - } - - Future createStack(List assetIds) async { - try { - return _api.stacksApi.createStack(StackCreateDto(assetIds: assetIds)); - } catch (error) { - dPrint(() => "Error while creating stack: $error"); - } - return null; - } - - Future updateStack(String stackId, String primaryAssetId) async { - try { - return await _api.stacksApi.updateStack(stackId, StackUpdateDto(primaryAssetId: primaryAssetId)); - } catch (error) { - dPrint(() => "Error while updating stack children: $error"); - } - return null; - } - - Future deleteStack(String stackId, List assets) async { - try { - await _api.stacksApi.deleteStack(stackId); - - // Update local database to trigger rerendering - final List removeAssets = []; - for (final asset in assets) { - asset.stackId = null; - asset.stackPrimaryAssetId = null; - asset.stackCount = 0; - - removeAssets.add(asset); - } - await _assetRepository.transaction(() => _assetRepository.updateAll(removeAssets)); - } catch (error) { - dPrint(() => "Error while deleting stack: $error"); - } - } -} - -final stackServiceProvider = Provider( - (ref) => StackService(ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider)), -); diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart deleted file mode 100644 index f5b55f36eb..0000000000 --- a/mobile/lib/services/sync.service.dart +++ /dev/null @@ -1,945 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/album.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; -import 'package:immich_mobile/repositories/partner.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:immich_mobile/services/hash.service.dart'; -import 'package:immich_mobile/utils/async_mutex.dart'; -import 'package:immich_mobile/utils/datetime_comparison.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:logging/logging.dart'; - -final syncServiceProvider = Provider( - (ref) => SyncService( - ref.watch(hashServiceProvider), - ref.watch(entityServiceProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(albumApiRepositoryProvider), - ref.watch(albumRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(exifRepositoryProvider), - ref.watch(partnerRepositoryProvider), - ref.watch(userRepositoryProvider), - ref.watch(userServiceProvider), - ref.watch(etagRepositoryProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(localFilesManagerRepositoryProvider), - ref.watch(partnerApiRepositoryProvider), - ref.watch(userApiRepositoryProvider), - ), -); - -class SyncService { - final HashService _hashService; - final EntityService _entityService; - final AlbumMediaRepository _albumMediaRepository; - final AlbumApiRepository _albumApiRepository; - final AlbumRepository _albumRepository; - final AssetRepository _assetRepository; - final IsarExifRepository _exifInfoRepository; - final IsarUserRepository _isarUserRepository; - final UserService _userService; - final PartnerRepository _partnerRepository; - final ETagRepository _eTagRepository; - final PartnerApiRepository _partnerApiRepository; - final UserApiRepository _userApiRepository; - final AsyncMutex _lock = AsyncMutex(); - final Logger _log = Logger('SyncService'); - final AppSettingsService _appSettingsService; - final LocalFilesManagerRepository _localFilesManager; - - SyncService( - this._hashService, - this._entityService, - this._albumMediaRepository, - this._albumApiRepository, - this._albumRepository, - this._assetRepository, - this._exifInfoRepository, - this._partnerRepository, - this._isarUserRepository, - this._userService, - this._eTagRepository, - this._appSettingsService, - this._localFilesManager, - this._partnerApiRepository, - this._userApiRepository, - ); - - // public methods: - - /// Syncs users from the server to the local database - /// Returns `true`if there were any changes - Future syncUsersFromServer(List users) => _lock.run(() => _syncUsersFromServer(users)); - - /// Syncs remote assets owned by the logged-in user to the DB - /// Returns `true` if there were any changes - Future syncRemoteAssetsToDb({ - required List users, - required Future<(List? toUpsert, List? toDelete)> Function(List users, DateTime since) - getChangedAssets, - required FutureOr?> Function(UserDto user, DateTime until) loadAssets, - }) => _lock.run( - () async => - await _syncRemoteAssetChanges(users, getChangedAssets) ?? - await _syncRemoteAssetsFull(getUsersFromServer, loadAssets), - ); - - /// Syncs remote albums to the database - /// returns `true` if there were any changes - Future syncRemoteAlbumsToDb(List remote) => _lock.run(() => _syncRemoteAlbumsToDb(remote)); - - /// Syncs all device albums and their assets to the database - /// Returns `true` if there were any changes - Future syncLocalAlbumAssetsToDb(List onDevice, [Set? excludedAssets]) => - _lock.run(() => _syncLocalAlbumAssetsToDb(onDevice, excludedAssets)); - - /// returns all Asset IDs that are not contained in the existing list - List sharedAssetsToRemove(List deleteCandidates, List existing) { - if (deleteCandidates.isEmpty) { - return []; - } - deleteCandidates.sort(Asset.compareById); - existing.sort(Asset.compareById); - return _diffAssets(existing, deleteCandidates, compare: Asset.compareById).$3.map((e) => e.id).toList(); - } - - /// Syncs a new asset to the db. Returns `true` if successful - Future syncNewAssetToDb(Asset newAsset) => _lock.run(() => _syncNewAssetToDb(newAsset)); - - Future removeAllLocalAlbumsAndAssets() => _lock.run(_removeAllLocalAlbumsAndAssets); - - // private methods: - - /// Syncs users from the server to the local database - /// Returns `true`if there were any changes - Future _syncUsersFromServer(List users) async { - users.sortBy((u) => u.id); - final dbUsers = await _isarUserRepository.getAll(sortBy: SortUserBy.id); - final List toDelete = []; - final List toUpsert = []; - final changes = diffSortedListsSync( - users, - dbUsers, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (UserDto a, UserDto b) { - 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) { - toUpsert.add(a); - return true; - } - return false; - }, - onlyFirst: (UserDto a) => toUpsert.add(a), - onlySecond: (UserDto b) => toDelete.add(b.id), - ); - if (changes) { - await _isarUserRepository.transaction(() async { - await _isarUserRepository.delete(toDelete); - await _isarUserRepository.updateAll(toUpsert); - }); - } - return changes; - } - - /// Syncs a new asset to the db. Returns `true` if successful - Future _syncNewAssetToDb(Asset a) async { - final Asset? inDb = await _assetRepository.getByOwnerIdChecksum(a.ownerId, a.checksum); - if (inDb != null) { - // unify local/remote assets by replacing the - // local-only asset in the DB with a local&remote asset - a = inDb.updatedCopy(a); - } - try { - await _assetRepository.update(a); - } catch (e) { - _log.severe("Failed to put new asset into db", e); - return false; - } - return true; - } - - /// Efficiently syncs assets via changes. Returns `null` when a full sync is required. - Future _syncRemoteAssetChanges( - List users, - Future<(List? toUpsert, List? toDelete)> Function(List users, DateTime since) - getChangedAssets, - ) async { - final currentUser = _userService.getMyUser(); - final DateTime? since = (await _eTagRepository.get(currentUser.id))?.time?.toUtc(); - if (since == null) return null; - final DateTime now = DateTime.now(); - final (toUpsert, toDelete) = await getChangedAssets(users, since); - if (toUpsert == null || toDelete == null) { - await _clearUserAssetsETag(users); - return null; - } - try { - if (toDelete.isNotEmpty) { - await handleRemoteAssetRemoval(toDelete); - } - if (toUpsert.isNotEmpty) { - final (_, updated) = await _linkWithExistingFromDb(toUpsert); - await upsertAssetsWithExif(updated); - } - if (toUpsert.isNotEmpty || toDelete.isNotEmpty) { - await _updateUserAssetsETag(users, now); - return true; - } - return false; - } catch (e) { - _log.severe("Failed to sync remote assets to db", e); - } - return null; - } - - Future _moveToTrashMatchedAssets(Iterable idsToDelete) async { - final List localAssets = await _assetRepository.getAllLocal(); - final List matchedAssets = localAssets.where((asset) => idsToDelete.contains(asset.remoteId)).toList(); - - final mediaUrls = await Future.wait(matchedAssets.map((asset) => asset.local?.getMediaUrl() ?? Future.value(null))); - - await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList()); - } - - /// Deletes remote-only assets, updates merged assets to be local-only - Future handleRemoteAssetRemoval(List idsToDelete) async { - return _assetRepository.transaction(() async { - await _assetRepository.deleteAllByRemoteId(idsToDelete, state: AssetState.remote); - final merged = await _assetRepository.getAllByRemoteId(idsToDelete, state: AssetState.merged); - if (Platform.isAndroid && _appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) { - await _moveToTrashMatchedAssets(idsToDelete); - } - if (merged.isEmpty) return; - for (final Asset asset in merged) { - asset.remoteId = null; - asset.isTrashed = false; - } - await _assetRepository.updateAll(merged); - }); - } - - Future> _getAllAccessibleUsers() async { - final sharedWith = (await _partnerRepository.getSharedWith()).toSet(); - sharedWith.add(_userService.getMyUser()); - return sharedWith.toList(); - } - - /// Syncs assets by loading and comparing all assets from the server. - Future _syncRemoteAssetsFull( - FutureOr?> Function() refreshUsers, - FutureOr?> Function(UserDto user, DateTime until) loadAssets, - ) async { - final serverUsers = await refreshUsers(); - if (serverUsers == null) { - _log.warning("_syncRemoteAssetsFull aborted because user refresh failed"); - return false; - } - await _syncUsersFromServer(serverUsers); - final List users = await _getAllAccessibleUsers(); - bool changes = false; - for (UserDto u in users) { - changes |= await _syncRemoteAssetsForUser(u, loadAssets); - } - return changes; - } - - Future _syncRemoteAssetsForUser( - UserDto user, - FutureOr?> Function(UserDto user, DateTime until) loadAssets, - ) async { - final DateTime now = DateTime.now().toUtc(); - final List? remote = await loadAssets(user, now); - if (remote == null) { - return false; - } - final List inDb = await _assetRepository.getAll(ownerId: user.id, sortBy: AssetSort.checksum); - assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); - - remote.sort(Asset.compareByChecksum); - - // filter our duplicates that might be introduced by the chunked retrieval - remote.uniqueConsecutive(compare: Asset.compareByChecksum); - - final (toAdd, toUpdate, toRemove) = _diffAssets(remote, inDb, remote: true); - if (toAdd.isEmpty && toUpdate.isEmpty && toRemove.isEmpty) { - await _updateUserAssetsETag([user], now); - return false; - } - final idsToDelete = toRemove.map((e) => e.id).toList(); - try { - await _assetRepository.deleteByIds(idsToDelete); - await upsertAssetsWithExif(toAdd + toUpdate); - } catch (e) { - _log.severe("Failed to sync remote assets to db", e); - } - await _updateUserAssetsETag([user], now); - return true; - } - - Future _updateUserAssetsETag(List users, DateTime time) { - final etags = users.map((u) => ETag(id: u.id, time: time)).toList(); - return _eTagRepository.upsertAll(etags); - } - - Future _clearUserAssetsETag(List users) { - final ids = users.map((u) => u.id).toList(); - return _eTagRepository.deleteByIds(ids); - } - - /// Syncs remote albums to the database - /// returns `true` if there were any changes - Future _syncRemoteAlbumsToDb(List remoteAlbums) async { - remoteAlbums.sortBy((e) => e.remoteId!); - - final List dbAlbums = await _albumRepository.getAll(remote: true, sortBy: AlbumSort.remoteId); - - final List toDelete = []; - final List existing = []; - - final bool changes = await diffSortedLists( - remoteAlbums, - dbAlbums, - compare: (remoteAlbum, dbAlbum) => remoteAlbum.remoteId!.compareTo(dbAlbum.remoteId!), - both: (remoteAlbum, dbAlbum) => _syncRemoteAlbum(remoteAlbum, dbAlbum, toDelete, existing), - onlyFirst: (remoteAlbum) => _addAlbumFromServer(remoteAlbum, existing), - onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete), - ); - - if (toDelete.isNotEmpty) { - final List idsToRemove = sharedAssetsToRemove(toDelete, existing); - if (idsToRemove.isNotEmpty) { - await _assetRepository.deleteByIds(idsToRemove); - } - } else { - assert(toDelete.isEmpty); - } - return changes; - } - - /// syncs albums from the server to the local database (does not support - /// syncing changes from local back to server) - /// accumulates - Future _syncRemoteAlbum(Album dto, Album album, List deleteCandidates, List existing) async { - if (!_hasRemoteAlbumChanged(dto, album)) { - return false; - } - // loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp, - // i.e. it will always be null. Save it here. - final originalDto = dto; - dto = await _albumApiRepository.get(dto.remoteId!); - - final assetsInDb = await _assetRepository.getByAlbum(album, sortBy: AssetSort.ownerIdChecksum); - assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!"); - final List assetsOnRemote = dto.remoteAssets.toList(); - assetsOnRemote.sort(Asset.compareByOwnerChecksum); - final (toAdd, toUpdate, toUnlink) = _diffAssets(assetsOnRemote, assetsInDb, compare: Asset.compareByOwnerChecksum); - - // update shared users - final List sharedUsers = album.sharedUsers.map((u) => u.toDto()).toList(growable: false); - sharedUsers.sort((a, b) => a.id.compareTo(b.id)); - final List users = dto.remoteUsers.map((u) => u.toDto()).toList()..sort((a, b) => a.id.compareTo(b.id)); - final List userIdsToAdd = []; - final List usersToUnlink = []; - diffSortedListsSync( - users, - sharedUsers, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (a, b) => false, - onlyFirst: (UserDto a) => userIdsToAdd.add(a.id), - onlySecond: (UserDto a) => usersToUnlink.add(a), - ); - - // for shared album: put missing album assets into local DB - final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd); - await upsertAssetsWithExif(updated); - final assetsToLink = existingInDb + updated; - final usersToLink = await _isarUserRepository.getByUserIds(userIdsToAdd); - - album.name = dto.name; - album.description = dto.description; - album.shared = dto.shared; - album.createdAt = dto.createdAt; - album.modifiedAt = dto.modifiedAt; - album.startDate = dto.startDate; - album.endDate = dto.endDate; - album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp; - album.shared = dto.shared; - album.activityEnabled = dto.activityEnabled; - album.sortOrder = dto.sortOrder; - - final remoteThumbnailAssetId = dto.remoteThumbnailAssetId; - if (remoteThumbnailAssetId != null && album.thumbnail.value?.remoteId != remoteThumbnailAssetId) { - album.thumbnail.value = await _assetRepository.getByRemoteId(remoteThumbnailAssetId); - } - - // write & commit all changes to DB - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(toUpdate); - await _albumRepository.addUsers(album, usersToLink.nonNulls.toList()); - await _albumRepository.removeUsers(album, usersToUnlink); - await _albumRepository.addAssets(album, assetsToLink); - await _albumRepository.removeAssets(album, toUnlink); - await _albumRepository.recalculateMetadata(album); - await _albumRepository.update(album); - }); - _log.info("Synced changes of remote album ${album.name} to DB"); - } catch (e) { - _log.severe("Failed to sync remote album to database", e); - } - - if (album.shared || dto.shared) { - final userId = (_userService.getMyUser()).id; - final foreign = await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); - existing.addAll(foreign); - - // delete assets in DB unless they belong to this user or part of some other shared album - final isarUserId = fastHash(userId); - deleteCandidates.addAll(toUnlink.where((a) => a.ownerId != isarUserId)); - } - - return true; - } - - /// Adds a remote album to the database while making sure to add any foreign - /// (shared) assets to the database beforehand - /// accumulates assets already existing in the database - Future _addAlbumFromServer(Album album, List existing) async { - if (album.remoteAssetCount != album.remoteAssets.length) { - album = await _albumApiRepository.get(album.remoteId!); - } - if (album.remoteAssetCount == album.remoteAssets.length) { - // in case an album contains assets not yet present in local DB: - // put missing album assets into local DB - final (existingInDb, updated) = await _linkWithExistingFromDb(album.remoteAssets.toList()); - existing.addAll(existingInDb); - await upsertAssetsWithExif(updated); - - await _entityService.fillAlbumWithDatabaseEntities(album); - await _albumRepository.create(album); - } else { - _log.warning( - "Failed to add album from server: assetCount ${album.remoteAssetCount} != " - "asset array length ${album.remoteAssets.length} for album ${album.name}", - ); - } - } - - /// Accumulates all suitable album assets to the `deleteCandidates` and - /// removes the album from the database. - Future _removeAlbumFromDb(Album album, List deleteCandidates) async { - if (album.isLocal) { - _log.info("Removing local album $album from DB"); - // delete assets in DB unless they are remote or part of some other album - deleteCandidates.addAll(await _assetRepository.getByAlbum(album, state: AssetState.local)); - } else if (album.shared) { - // delete assets in DB unless they belong to this user or are part of some other shared album or belong to a partner - final userIds = (await _getAllAccessibleUsers()).map((user) => user.id); - final orphanedAssets = await _assetRepository.getByAlbum(album, notOwnedBy: userIds); - deleteCandidates.addAll(orphanedAssets); - } - try { - await _albumRepository.delete(album.id); - _log.info("Removed local album $album from DB"); - } catch (e) { - _log.severe("Failed to remove local album $album from DB", e); - } - } - - /// Syncs all device albums and their assets to the database - /// Returns `true` if there were any changes - Future _syncLocalAlbumAssetsToDb(List onDevice, [Set? excludedAssets]) async { - onDevice.sort((a, b) => a.localId!.compareTo(b.localId!)); - final inDb = await _albumRepository.getAll(remote: false, sortBy: AlbumSort.localId); - final List deleteCandidates = []; - final List existing = []; - final bool anyChanges = await diffSortedLists( - onDevice, - inDb, - compare: (Album a, Album b) => a.localId!.compareTo(b.localId!), - both: (Album a, Album b) => _syncAlbumInDbAndOnDevice(a, b, deleteCandidates, existing, excludedAssets), - onlyFirst: (Album a) => _addAlbumFromDevice(a, existing, excludedAssets), - onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates), - ); - _log.fine("Syncing all local albums almost done. Collected ${deleteCandidates.length} asset candidates to delete"); - final (toDelete, toUpdate) = _handleAssetRemoval(deleteCandidates, existing, remote: false); - _log.fine("${toDelete.length} assets to delete, ${toUpdate.length} to update"); - if (toDelete.isNotEmpty || toUpdate.isNotEmpty) { - await _assetRepository.transaction(() async { - await _assetRepository.deleteByIds(toDelete); - await _assetRepository.updateAll(toUpdate); - }); - _log.info("Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB"); - } - return anyChanges; - } - - /// Syncs the device album to the album in the database - /// returns `true` if there were any changes - /// Accumulates asset candidates to delete and those already existing in DB - Future _syncAlbumInDbAndOnDevice( - Album deviceAlbum, - Album dbAlbum, - List deleteCandidates, - List existing, [ - Set? excludedAssets, - bool forceRefresh = false, - ]) async { - _log.info("Syncing a local album to DB: ${deviceAlbum.name}"); - if (!forceRefresh && !await _hasAlbumChangeOnDevice(deviceAlbum, dbAlbum)) { - _log.info("Local album ${deviceAlbum.name} has not changed. Skipping sync."); - return false; - } - _log.info("Local album ${deviceAlbum.name} has changed. Syncing..."); - if (!forceRefresh && excludedAssets == null && await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) { - _log.info("Fast synced local album ${deviceAlbum.name} to DB"); - return true; - } - // general case, e.g. some assets have been deleted or there are excluded albums on iOS - final inDb = await _assetRepository.getByAlbum( - dbAlbum, - ownerId: (_userService.getMyUser()).id, - sortBy: AssetSort.checksum, - ); - - assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); - final int assetCountOnDevice = await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); - final List onDevice = await _getHashedAssets(deviceAlbum, excludedAssets: excludedAssets); - _removeDuplicates(onDevice); - // _removeDuplicates sorts `onDevice` by checksum - final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb); - if (toAdd.isEmpty && - toUpdate.isEmpty && - toDelete.isEmpty && - dbAlbum.name == deviceAlbum.name && - dbAlbum.description == deviceAlbum.description && - dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) { - // changes only affeted excluded albums - _log.info("Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync."); - if (assetCountOnDevice != (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount) { - await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: assetCountOnDevice)]); - } - return false; - } - _log.info( - "Syncing local album ${deviceAlbum.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete", - ); - final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd); - _log.info( - "Linking assets to add with existing from db. ${existingInDb.length} existing, ${updated.length} to update", - ); - deleteCandidates.addAll(toDelete); - existing.addAll(existingInDb); - dbAlbum.name = deviceAlbum.name; - dbAlbum.description = deviceAlbum.description; - dbAlbum.modifiedAt = deviceAlbum.modifiedAt; - if (dbAlbum.thumbnail.value != null && toDelete.contains(dbAlbum.thumbnail.value)) { - dbAlbum.thumbnail.value = null; - } - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(updated + toUpdate); - await _albumRepository.addAssets(dbAlbum, existingInDb + updated); - await _albumRepository.removeAssets(dbAlbum, toDelete); - await _albumRepository.recalculateMetadata(dbAlbum); - await _albumRepository.update(dbAlbum); - await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: assetCountOnDevice)]); - }); - _log.info("Synced changes of local album ${deviceAlbum.name} to DB"); - } catch (e) { - _log.severe("Failed to update synced album ${deviceAlbum.name} in DB", e); - } - - return true; - } - - /// fast path for common case: only new assets were added to device album - /// returns `true` if successful, else `false` - Future _syncDeviceAlbumFast(Album deviceAlbum, Album dbAlbum) async { - if (!deviceAlbum.modifiedAt.isAfter(dbAlbum.modifiedAt)) { - _log.info("Local album ${deviceAlbum.name} has not changed. Skipping sync."); - return false; - } - final int totalOnDevice = await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); - final int lastKnownTotal = (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount ?? 0; - if (totalOnDevice <= lastKnownTotal) { - _log.info("Local album ${deviceAlbum.name} totalOnDevice is less than lastKnownTotal. Skipping sync."); - return false; - } - final List newAssets = await _getHashedAssets( - deviceAlbum, - modifiedFrom: dbAlbum.modifiedAt.add(const Duration(seconds: 1)), - modifiedUntil: deviceAlbum.modifiedAt, - ); - - if (totalOnDevice != lastKnownTotal + newAssets.length) { - _log.info( - "Local album ${deviceAlbum.name} totalOnDevice is not equal to lastKnownTotal + newAssets.length. Skipping sync.", - ); - return false; - } - dbAlbum.modifiedAt = deviceAlbum.modifiedAt; - _removeDuplicates(newAssets); - final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets); - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(updated); - await _albumRepository.addAssets(dbAlbum, existingInDb + updated); - await _albumRepository.recalculateMetadata(dbAlbum); - await _albumRepository.update(dbAlbum); - await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice)]); - }); - _log.info("Fast synced local album ${deviceAlbum.name} to DB"); - } catch (e) { - _log.severe("Failed to fast sync local album ${deviceAlbum.name} to DB", e); - return false; - } - - return true; - } - - /// Adds a new album from the device to the database and Accumulates all - /// assets already existing in the database to the list of `existing` assets - Future _addAlbumFromDevice(Album album, List existing, [Set? excludedAssets]) async { - _log.info("Adding a new local album to DB: ${album.name}"); - final assets = await _getHashedAssets(album, excludedAssets: excludedAssets); - _removeDuplicates(assets); - final (existingInDb, updated) = await _linkWithExistingFromDb(assets); - _log.info("${existingInDb.length} assets already existed in DB, to upsert ${updated.length}"); - await upsertAssetsWithExif(updated); - existing.addAll(existingInDb); - album.assets.addAll(existingInDb); - album.assets.addAll(updated); - final thumb = existingInDb.firstOrNull ?? updated.firstOrNull; - album.thumbnail.value = thumb; - try { - await _albumRepository.create(album); - final int assetCount = await _albumMediaRepository.getAssetCount(album.localId!); - await _eTagRepository.upsertAll([ETag(id: album.eTagKeyAssetCount, assetCount: assetCount)]); - _log.info("Added a new local album to DB: ${album.name}"); - } catch (e) { - _log.severe("Failed to add new local album ${album.name} to DB", e); - } - } - - /// Returns a tuple (existing, updated) - Future<(List existing, List updated)> _linkWithExistingFromDb(List assets) async { - if (assets.isEmpty) return ([].cast(), [].cast()); - - final List inDb = await _assetRepository.getAllByOwnerIdChecksum( - assets.map((a) => a.ownerId).toInt64List(), - assets.map((a) => a.checksum).toList(growable: false), - ); - assert(inDb.length == assets.length); - final List existing = [], toUpsert = []; - for (int i = 0; i < assets.length; i++) { - final Asset? b = inDb[i]; - if (b == null) { - toUpsert.add(assets[i]); - continue; - } - if (b.canUpdate(assets[i])) { - final updated = b.updatedCopy(assets[i]); - assert(updated.isInDb); - toUpsert.add(updated); - } else { - existing.add(b); - } - } - assert(existing.length + toUpsert.length == assets.length); - return (existing, toUpsert); - } - - Future _toggleTrashStatusForAssets(List assetsList) async { - final trashMediaUrls = []; - - for (final asset in assetsList) { - if (asset.isTrashed) { - final mediaUrl = await asset.local?.getMediaUrl(); - if (mediaUrl == null) { - _log.warning("Failed to get media URL for asset ${asset.name} while moving to trash"); - continue; - } - trashMediaUrls.add(mediaUrl); - } else { - await _localFilesManager.restoreFromTrash(asset.fileName, asset.type.index); - } - } - - if (trashMediaUrls.isNotEmpty) { - await _localFilesManager.moveToTrash(trashMediaUrls); - } - } - - /// Inserts or updates the assets in the database with their ExifInfo (if any) - Future upsertAssetsWithExif(List assets) async { - if (assets.isEmpty) return; - - if (Platform.isAndroid && _appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) { - await _toggleTrashStatusForAssets(assets); - } - - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(assets); - for (final Asset added in assets) { - added.exifInfo = added.exifInfo?.copyWith(assetId: added.id); - } - final exifInfos = assets.map((e) => e.exifInfo).nonNulls.toList(); - await _exifInfoRepository.updateAll(exifInfos); - }); - _log.info("Upserted ${assets.length} assets into the DB"); - } catch (e) { - _log.severe("Failed to upsert ${assets.length} assets into the DB", e); - // give details on the errors - assets.sort(Asset.compareByOwnerChecksum); - final inDb = await _assetRepository.getAllByOwnerIdChecksum( - assets.map((e) => e.ownerId).toInt64List(), - assets.map((e) => e.checksum).toList(growable: false), - ); - for (int i = 0; i < assets.length; i++) { - final Asset a = assets[i]; - final Asset? b = inDb[i]; - if (b == null) { - if (!a.isInDb) { - _log.warning("Trying to update an asset that does not exist in DB:\n$a"); - } - } else if (a.id != b.id) { - _log.warning("Trying to insert another asset with the same checksum+owner. In DB:\n$b\nTo insert:\n$a"); - } - } - for (int i = 1; i < assets.length; i++) { - if (Asset.compareByOwnerChecksum(assets[i - 1], assets[i]) == 0) { - _log.warning("Trying to insert duplicate assets:\n${assets[i - 1]}\n${assets[i]}"); - } - } - } - } - - /// Returns all assets that were successfully hashed - Future> _getHashedAssets( - Album album, { - int start = 0, - int end = 0x7fffffffffffffff, - DateTime? modifiedFrom, - DateTime? modifiedUntil, - Set? excludedAssets, - }) async { - final entities = await _albumMediaRepository.getAssets( - album.localId!, - start: start, - end: end, - modifiedFrom: modifiedFrom, - modifiedUntil: modifiedUntil, - ); - final filtered = excludedAssets == null - ? entities - : entities.where((e) => !excludedAssets.contains(e.localId!)).toList(); - return _hashService.hashAssets(filtered); - } - - List _removeDuplicates(List assets) { - final int before = assets.length; - assets.sort(Asset.compareByOwnerChecksumCreatedModified); - assets.uniqueConsecutive(compare: Asset.compareByOwnerChecksum, onDuplicate: (a, b) => {}); - final int duplicates = before - assets.length; - if (duplicates > 0) { - _log.warning("Ignored $duplicates duplicate assets on device"); - } - return assets; - } - - /// returns `true` if the albums differ on the surface - Future _hasAlbumChangeOnDevice(Album deviceAlbum, Album dbAlbum) async { - return deviceAlbum.name != dbAlbum.name || - deviceAlbum.description != dbAlbum.description || - !deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) || - await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) != - (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount; - } - - Future _removeAllLocalAlbumsAndAssets() async { - try { - final assets = await _assetRepository.getAllLocal(); - final (toDelete, toUpdate) = _handleAssetRemoval(assets, [], remote: false); - await _assetRepository.transaction(() async { - await _assetRepository.deleteByIds(toDelete); - await _assetRepository.updateAll(toUpdate); - await _albumRepository.deleteAllLocal(); - }); - return true; - } catch (e) { - _log.severe("Failed to remove all local albums and assets", e); - return false; - } - } - - Future?> getUsersFromServer() async { - List? users; - try { - users = await _userApiRepository.getAll(); - } catch (e) { - _log.warning("Failed to fetch users", e); - users = null; - } - final List sharedBy = await _partnerApiRepository.getAll(Direction.sharedByMe); - final List sharedWith = await _partnerApiRepository.getAll(Direction.sharedWithMe); - - if (users == null) { - _log.warning("Failed to refresh users"); - return null; - } - - users.sortBy((u) => u.id); - sharedBy.sortBy((u) => u.id); - sharedWith.sortBy((u) => u.id); - - final updatedSharedBy = []; - - diffSortedListsSync( - users, - sharedBy, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (UserDto a, UserDto b) { - updatedSharedBy.add(a.copyWith(isPartnerSharedBy: true)); - return true; - }, - onlyFirst: (UserDto a) => updatedSharedBy.add(a), - onlySecond: (UserDto b) => updatedSharedBy.add(b), - ); - - final updatedSharedWith = []; - - diffSortedListsSync( - updatedSharedBy, - sharedWith, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (UserDto a, UserDto b) { - updatedSharedWith.add(a.copyWith(inTimeline: b.inTimeline, isPartnerSharedWith: true)); - return true; - }, - onlyFirst: (UserDto a) => updatedSharedWith.add(a), - onlySecond: (UserDto b) => updatedSharedWith.add(b), - ); - - return updatedSharedWith; - } -} - -/// Returns a triple(toAdd, toUpdate, toRemove) -(List toAdd, List toUpdate, List toRemove) _diffAssets( - List assets, - List inDb, { - bool? remote, - int Function(Asset, Asset) compare = Asset.compareByChecksum, -}) { - // fast paths for trivial cases: reduces memory usage during initial sync etc. - if (assets.isEmpty && inDb.isEmpty) { - return const ([], [], []); - } else if (assets.isEmpty && remote == null) { - // remove all from database - return (const [], const [], inDb); - } else if (inDb.isEmpty) { - // add all assets - return (assets, const [], const []); - } - - final List toAdd = []; - final List toUpdate = []; - final List toRemove = []; - diffSortedListsSync( - inDb, - assets, - compare: compare, - both: (Asset a, Asset b) { - if (a.canUpdate(b)) { - toUpdate.add(a.updatedCopy(b)); - return true; - } - return false; - }, - onlyFirst: (Asset a) { - if (remote == true && a.isLocal) { - if (a.remoteId != null) { - a.remoteId = null; - toUpdate.add(a); - } - } else if (remote == false && a.isRemote) { - if (a.isLocal) { - a.localId = null; - toUpdate.add(a); - } - } else { - toRemove.add(a); - } - }, - onlySecond: (Asset b) => toAdd.add(b), - ); - return (toAdd, toUpdate, toRemove); -} - -/// returns a tuple (toDelete toUpdate) when assets are to be deleted -(List toDelete, List toUpdate) _handleAssetRemoval( - List deleteCandidates, - List existing, { - bool? remote, -}) { - if (deleteCandidates.isEmpty) { - return const ([], []); - } - deleteCandidates.sort(Asset.compareById); - deleteCandidates.uniqueConsecutive(compare: Asset.compareById); - existing.sort(Asset.compareById); - existing.uniqueConsecutive(compare: Asset.compareById); - final (tooAdd, toUpdate, toRemove) = _diffAssets( - existing, - deleteCandidates, - compare: Asset.compareById, - remote: remote, - ); - assert(tooAdd.isEmpty, "toAdd should be empty in _handleAssetRemoval"); - return (toRemove.map((e) => e.id).toList(), toUpdate); -} - -/// returns `true` if the albums differ on the surface -bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) { - return remoteAlbum.remoteAssetCount != dbAlbum.assetCount || - remoteAlbum.name != dbAlbum.name || - remoteAlbum.description != dbAlbum.description || - remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId || - remoteAlbum.shared != dbAlbum.shared || - remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length || - !remoteAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) || - !isAtSameMomentAs(remoteAlbum.startDate, dbAlbum.startDate) || - !isAtSameMomentAs(remoteAlbum.endDate, dbAlbum.endDate) || - !isAtSameMomentAs(remoteAlbum.lastModifiedAssetTimestamp, dbAlbum.lastModifiedAssetTimestamp); -} diff --git a/mobile/lib/services/timeline.service.dart b/mobile/lib/services/timeline.service.dart deleted file mode 100644 index eaff1027d8..0000000000 --- a/mobile/lib/services/timeline.service.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/timeline.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; - -final timelineServiceProvider = Provider((ref) { - return TimelineService( - ref.watch(timelineRepositoryProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(userServiceProvider), - ); -}); - -class TimelineService { - final TimelineRepository _timelineRepository; - final AppSettingsService _appSettingsService; - final UserService _userService; - - const TimelineService(this._timelineRepository, this._appSettingsService, this._userService); - - Future> getTimelineUserIds() async { - final me = _userService.getMyUser(); - return _timelineRepository.getTimelineUserIds(me.id); - } - - Stream> watchTimelineUserIds() async* { - final me = _userService.getMyUser(); - yield* _timelineRepository.watchTimelineUsers(me.id); - } - - Stream watchHomeTimeline(String userId) { - return _timelineRepository.watchHomeTimeline(userId, _getGroupByOption()); - } - - Stream watchMultiUsersTimeline(List userIds) { - return _timelineRepository.watchMultiUsersTimeline(userIds, _getGroupByOption()); - } - - Stream watchArchiveTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchArchiveTimeline(user.id); - } - - Stream watchFavoriteTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchFavoriteTimeline(user.id); - } - - Stream watchAlbumTimeline(Album album) async* { - yield* _timelineRepository.watchAlbumTimeline(album, _getGroupByOption()); - } - - Stream watchTrashTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchTrashTimeline(user.id); - } - - Stream watchAllVideosTimeline() { - final user = _userService.getMyUser(); - - return _timelineRepository.watchAllVideosTimeline(user.id); - } - - Future getTimelineFromAssets(List assets, GroupAssetsBy? groupBy) { - GroupAssetsBy groupOption = GroupAssetsBy.none; - if (groupBy == null) { - groupOption = _getGroupByOption(); - } else { - groupOption = groupBy; - } - - return _timelineRepository.getTimelineFromAssets(assets, groupOption); - } - - Stream watchAssetSelectionTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchAssetSelectionTimeline(user.id); - } - - GroupAssetsBy _getGroupByOption() { - return GroupAssetsBy.values[_appSettingsService.getSetting(AppSettingsEnum.groupAssetsBy)]; - } - - Stream watchLockedTimelineProvider() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchLockedTimeline(user.id, _getGroupByOption()); - } -} diff --git a/mobile/lib/services/trash.service.dart b/mobile/lib/services/trash.service.dart deleted file mode 100644 index 2c51a68c59..0000000000 --- a/mobile/lib/services/trash.service.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:openapi/api.dart'; - -final trashServiceProvider = Provider((ref) { - return TrashService( - ref.watch(apiServiceProvider), - ref.watch(assetRepositoryProvider), - ref.watch(userServiceProvider), - ); -}); - -class TrashService { - final ApiService _apiService; - final AssetRepository _assetRepository; - final UserService _userService; - - const TrashService(this._apiService, this._assetRepository, this._userService); - - Future restoreAssets(Iterable assetList) async { - final remoteAssets = assetList.where((a) => a.isRemote); - await _apiService.trashApi.restoreAssets(BulkIdsDto(ids: remoteAssets.map((e) => e.remoteId!).toList())); - - final updatedAssets = remoteAssets.map((asset) { - asset.isTrashed = false; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } - - Future emptyTrash() async { - final user = _userService.getMyUser(); - - await _apiService.trashApi.emptyTrash(); - - final trashedAssets = await _assetRepository.getTrashAssets(user.id); - final ids = trashedAssets.map((e) => e.remoteId!).toList(); - - await _assetRepository.transaction(() async { - await _assetRepository.deleteAllByRemoteId(ids, state: AssetState.remote); - - final merged = await _assetRepository.getAllByRemoteId(ids, state: AssetState.merged); - if (merged.isEmpty) { - return; - } - - for (final Asset asset in merged) { - asset.remoteId = null; - asset.isTrashed = false; - } - - await _assetRepository.updateAll(merged); - }); - } - - Future restoreTrash() async { - final user = _userService.getMyUser(); - - await _apiService.trashApi.restoreTrash(); - - final trashedAssets = await _assetRepository.getTrashAssets(user.id); - final updatedAssets = trashedAssets.map((asset) { - asset.isTrashed = false; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } -} diff --git a/mobile/lib/theme/dynamic_theme.dart b/mobile/lib/theme/dynamic_theme.dart index d0cb8e646f..7f7c4d05d7 100644 --- a/mobile/lib/theme/dynamic_theme.dart +++ b/mobile/lib/theme/dynamic_theme.dart @@ -19,8 +19,16 @@ abstract final class DynamicTheme { // Some palettes do not generate surface container colors accurately, // so we regenerate all colors using the primary color _theme = ImmichTheme( - light: ColorScheme.fromSeed(seedColor: primaryColor, brightness: Brightness.light), - dark: ColorScheme.fromSeed(seedColor: primaryColor, brightness: Brightness.dark), + light: ColorScheme.fromSeed( + seedColor: primaryColor, + brightness: Brightness.light, + dynamicSchemeVariant: DynamicSchemeVariant.fidelity, + ), + dark: ColorScheme.fromSeed( + seedColor: primaryColor, + brightness: Brightness.dark, + dynamicSchemeVariant: DynamicSchemeVariant.fidelity, + ), ); } } catch (error) { diff --git a/mobile/lib/theme/theme_data.dart b/mobile/lib/theme/theme_data.dart index 69b8596490..7200d58dca 100644 --- a/mobile/lib/theme/theme_data.dart +++ b/mobile/lib/theme/theme_data.dart @@ -62,6 +62,7 @@ ThemeData getThemeData({required ColorScheme colorScheme, required Locale locale ), chipTheme: const ChipThemeData(side: BorderSide.none), sliderTheme: const SliderThemeData( + trackHeight: 12, // ignore: deprecated_member_use year2023: false, ), diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 071956392c..2aad2f264a 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -143,8 +143,7 @@ enum ActionButtonType { !context.isInLockedView && // context.currentAlbum != null, ActionButtonType.setAlbumCover => - context.isOwner && // - !context.isInLockedView && // + !context.isInLockedView && // context.currentAlbum != null && // context.selectedCount == 1, ActionButtonType.unstack => diff --git a/mobile/lib/utils/backup_progress.dart b/mobile/lib/utils/backup_progress.dart deleted file mode 100644 index 36050f5e20..0000000000 --- a/mobile/lib/utils/backup_progress.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; - -import 'package:easy_localization/easy_localization.dart'; - -final NumberFormat numberFormat = NumberFormat("###0.##"); - -String formatAssetBackupProgress(int uploadedAssets, int assetsToUpload) { - final int percent = (uploadedAssets * 100) ~/ assetsToUpload; - return "$percent% ($uploadedAssets/$assetsToUpload)"; -} - -/// prints progress in useful (kilo/mega/giga)bytes -String humanReadableFileBytesProgress(int bytes, int bytesTotal) { - String unit = "KB"; - - if (bytesTotal >= 0x40000000) { - unit = "GB"; - bytes >>= 20; - bytesTotal >>= 20; - } else if (bytesTotal >= 0x100000) { - unit = "MB"; - bytes >>= 10; - bytesTotal >>= 10; - } else if (bytesTotal < 0x400) { - return "${(bytes).toStringAsFixed(2)} B / ${(bytesTotal).toStringAsFixed(2)} B"; - } - - return "${(bytes / 1024.0).toStringAsFixed(2)} $unit / ${(bytesTotal / 1024.0).toStringAsFixed(2)} $unit"; -} - -/// prints percentage and absolute progress in useful (kilo/mega/giga)bytes -String humanReadableBytesProgress(int bytes, int bytesTotal) { - String unit = "KB"; // Kilobyte - if (bytesTotal >= 0x40000000) { - unit = "GB"; // Gigabyte - bytes >>= 20; - bytesTotal >>= 20; - } else if (bytesTotal >= 0x100000) { - unit = "MB"; // Megabyte - bytes >>= 10; - bytesTotal >>= 10; - } else if (bytesTotal < 0x400) { - return "$bytes / $bytesTotal B"; - } - final int percent = (bytes * 100) ~/ bytesTotal; - final String done = numberFormat.format(bytes / 1024.0); - final String total = numberFormat.format(bytesTotal / 1024.0); - return "$percent% ($done/$total$unit)"; -} - -class ThrottleProgressUpdate { - ThrottleProgressUpdate(this._fun, Duration interval) : _interval = interval.inMicroseconds; - final void Function(String?, int, int) _fun; - final int _interval; - int _invokedAt = 0; - Timer? _timer; - - String? title; - int progress = 0; - int total = 0; - - void call({final String? title, final int progress = 0, final int total = 0}) { - final time = Timeline.now; - this.title = title ?? this.title; - this.progress = progress; - this.total = total; - if (time > _invokedAt + _interval) { - _timer?.cancel(); - _onTimeElapsed(); - } else { - _timer ??= Timer(Duration(microseconds: _interval), _onTimeElapsed); - } - } - - void _onTimeElapsed() { - _invokedAt = Timeline.now; - _fun(title, progress, total); - _timer = null; - // clear title to not send/overwrite it next time if unchanged - title = null; - } -} diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index d63a92ba37..e79b06f53b 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -1,30 +1,14 @@ -import 'dart:io'; - import 'package:background_downloader/background_downloader.dart'; -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:isar/isar.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:photo_manager/photo_manager.dart'; void configureFileDownloaderNotifications() { FileDownloader().configureNotificationForGroup( @@ -57,48 +41,10 @@ void configureFileDownloaderNotifications() { } abstract final class Bootstrap { - static Future<(Isar isar, Drift drift, DriftLogger logDb)> initDB() async { + static Future<(Drift, DriftLogger)> initDomain({bool listenStoreUpdates = true, bool shouldBufferLogs = true}) async { final drift = Drift(); final logDb = DriftLogger(); - - Isar? isar = Isar.getInstance(); - - if (isar != null) { - return (isar, drift, logDb); - } - - final dir = await getApplicationDocumentsDirectory(); - isar = await Isar.open( - [ - StoreValueSchema, - AssetSchema, - AlbumSchema, - ExifInfoSchema, - UserSchema, - BackupAlbumSchema, - DuplicatedAssetSchema, - ETagSchema, - if (Platform.isAndroid) AndroidDeviceAssetSchema, - if (Platform.isIOS) IOSDeviceAssetSchema, - DeviceAssetEntitySchema, - ], - directory: dir.path, - maxSizeMiB: 2048, - inspector: kDebugMode, - ); - - return (isar, drift, logDb); - } - - 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); + final DriftStoreRepository storeRepo = DriftStoreRepository(drift); await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates); @@ -109,5 +55,8 @@ abstract final class Bootstrap { ); await NetworkRepository.init(); + // Remove once all asset operations are migrated to Native APIs + await PhotoManager.setIgnorePermissionCheck(true); + return (drift, logDb); } } diff --git a/mobile/lib/utils/cache/custom_image_cache.dart b/mobile/lib/utils/cache/custom_image_cache.dart index 99ce0db57c..2c09030ffa 100644 --- a/mobile/lib/utils/cache/custom_image_cache.dart +++ b/mobile/lib/utils/cache/custom_image_cache.dart @@ -20,7 +20,7 @@ final class CustomImageCache implements ImageCache { set maximumSize(int value) => _small.maximumSize = value; @override - set maximumSizeBytes(int value) => _small.maximumSize = value; + set maximumSizeBytes(int value) => _small.maximumSizeBytes = value; @override void clear() { diff --git a/mobile/lib/utils/color_filter_generator.dart b/mobile/lib/utils/color_filter_generator.dart deleted file mode 100644 index 92aed4b1a0..0000000000 --- a/mobile/lib/utils/color_filter_generator.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class InvertionFilter extends StatelessWidget { - final Widget? child; - const InvertionFilter({super.key, this.child}); - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: const ColorFilter.matrix([ - -1, 0, 0, 0, 255, // - 0, -1, 0, 0, 255, // - 0, 0, -1, 0, 255, // - 0, 0, 0, 1, 0, // - ]), - child: child, - ); - } -} - -// -1 - darkest, 1 - brightest, 0 - unchanged -class BrightnessFilter extends StatelessWidget { - final Widget? child; - final double brightness; - const BrightnessFilter({super.key, this.child, this.brightness = 0}); - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: ColorFilter.matrix(_ColorFilterGenerator.brightnessAdjustMatrix(brightness)), - child: child, - ); - } -} - -// -1 - greyscale, 1 - most saturated, 0 - unchanged -class SaturationFilter extends StatelessWidget { - final Widget? child; - final double saturation; - const SaturationFilter({super.key, this.child, this.saturation = 0}); - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: ColorFilter.matrix(_ColorFilterGenerator.saturationAdjustMatrix(saturation)), - child: child, - ); - } -} - -class _ColorFilterGenerator { - static List brightnessAdjustMatrix(double value) { - value = value * 10; - - if (value == 0) { - return [ - 1, 0, 0, 0, 0, // - 0, 1, 0, 0, 0, // - 0, 0, 1, 0, 0, // - 0, 0, 0, 1, 0, // - ]; - } - - return List.from([ - 1, 0, 0, 0, value, 0, 1, 0, 0, value, 0, 0, 1, 0, value, 0, 0, 0, 1, 0, // - ]).map((i) => i.toDouble()).toList(); - } - - static List saturationAdjustMatrix(double value) { - value = value * 100; - - if (value == 0) { - return [ - 1, 0, 0, 0, 0, // - 0, 1, 0, 0, 0, // - 0, 0, 1, 0, 0, // - 0, 0, 0, 1, 0, // - ]; - } - - double x = ((1 + ((value > 0) ? ((3 * value) / 100) : (value / 100)))).toDouble(); - double lumR = 0.3086; - double lumG = 0.6094; - double lumB = 0.082; - - return List.from([ - (lumR * (1 - x)) + x, lumG * (1 - x), lumB * (1 - x), // - 0, 0, // - lumR * (1 - x), // - (lumG * (1 - x)) + x, // - lumB * (1 - x), // - 0, 0, // - lumR * (1 - x), // - lumG * (1 - x), // - (lumB * (1 - x)) + x, // - 0, 0, 0, 0, 0, 1, 0, // - ]).map((i) => i.toDouble()).toList(); - } -} diff --git a/mobile/lib/utils/datetime_comparison.dart b/mobile/lib/utils/datetime_comparison.dart deleted file mode 100644 index f8ddcfea11..0000000000 --- a/mobile/lib/utils/datetime_comparison.dart +++ /dev/null @@ -1,2 +0,0 @@ -bool isAtSameMomentAs(DateTime? a, DateTime? b) => - (a == null && b == null) || ((a != null && b != null) && a.isAtSameMomentAs(b)); diff --git a/mobile/lib/utils/editor.utils.dart b/mobile/lib/utils/editor.utils.dart new file mode 100644 index 0000000000..fa2dedf383 --- /dev/null +++ b/mobile/lib/utils/editor.utils.dart @@ -0,0 +1,65 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/utils/matrix.utils.dart'; +import 'package:openapi/api.dart' hide AssetEditAction; + +Rect convertCropParametersToRect(CropParameters parameters, int originalWidth, int originalHeight) { + return Rect.fromLTWH( + parameters.x.toDouble() / originalWidth, + parameters.y.toDouble() / originalHeight, + parameters.width.toDouble() / originalWidth, + parameters.height.toDouble() / originalHeight, + ); +} + +CropParameters convertRectToCropParameters(Rect rect, int originalWidth, int originalHeight) { + final x = (rect.left * originalWidth).truncate(); + final y = (rect.top * originalHeight).truncate(); + final width = (rect.width * originalWidth).truncate(); + final height = (rect.height * originalHeight).truncate(); + + return CropParameters( + x: max(x, 0).clamp(0, originalWidth), + y: max(y, 0).clamp(0, originalHeight), + width: max(width, 0).clamp(0, originalWidth - x), + height: max(height, 0).clamp(0, originalHeight - y), + ); +} + +AffineMatrix buildAffineFromEdits(List edits) { + return AffineMatrix.compose( + edits.map((edit) { + return switch (edit) { + RotateEdit(:final parameters) => AffineMatrix.rotate(parameters.angle * pi / 180), + MirrorEdit(:final parameters) => + parameters.axis == MirrorAxis.horizontal ? AffineMatrix.flipY() : AffineMatrix.flipX(), + CropEdit() => AffineMatrix.identity(), + }; + }).toList(), + ); +} + +bool isCloseToZero(double value, [double epsilon = 1e-15]) { + return value.abs() < epsilon; +} + +typedef NormalizedTransform = ({double rotation, bool mirrorHorizontal, bool mirrorVertical}); + +NormalizedTransform normalizeTransformEdits(List edits) { + final matrix = buildAffineFromEdits(edits); + + double a = matrix.a; + double b = matrix.b; + double c = matrix.c; + double d = matrix.d; + + final rotation = ((isCloseToZero(a) ? asin(c) : acos(a)) * 180) / pi; + + return ( + rotation: rotation < 0 ? 360 + rotation : rotation, + mirrorHorizontal: false, + mirrorVertical: isCloseToZero(a) ? b == c : a == -d, + ); +} diff --git a/mobile/lib/utils/hooks/blurhash_hook.dart b/mobile/lib/utils/hooks/blurhash_hook.dart index ac5fd31724..534c0ad8fb 100644 --- a/mobile/lib/utils/hooks/blurhash_hook.dart +++ b/mobile/lib/utils/hooks/blurhash_hook.dart @@ -1,20 +1,10 @@ import 'dart:convert'; import 'dart:typed_data'; + import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:thumbhash/thumbhash.dart' as thumbhash; -ObjectRef useBlurHashRef(Asset? asset) { - if (asset?.thumbhash == null) { - return useRef(null); - } - - final rbga = thumbhash.thumbHashToRGBA(base64Decode(asset!.thumbhash!)); - - return useRef(thumbhash.rgbaToBmp(rbga)); -} - ObjectRef useDriftBlurHashRef(RemoteAsset? asset) { if (asset?.thumbHash == null) { return useRef(null); diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index 079f0e51fa..c562049b1d 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -1,47 +1,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:openapi/api.dart'; -String getThumbnailUrl(final Asset asset, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - return getThumbnailUrlForRemoteId(asset.remoteId!, type: type); -} - -String getThumbnailCacheKey(final Asset asset, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - return getThumbnailCacheKeyForRemoteId(asset.remoteId!, asset.thumbhash!, type: type); -} - -String getThumbnailCacheKeyForRemoteId( - final String id, - final String thumbhash, { - AssetMediaSize type = AssetMediaSize.thumbnail, -}) { - if (type == AssetMediaSize.thumbnail) { - return 'thumbnail-image-$id-$thumbhash'; - } else { - return '${id}_${thumbhash}_previewStage'; - } -} - -String getAlbumThumbnailUrl(final Album album, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - if (album.thumbnail.value?.remoteId == null) { - return ''; - } - return getThumbnailUrlForRemoteId(album.thumbnail.value!.remoteId!, type: type); -} - -String getAlbumThumbNailCacheKey(final Album album, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - if (album.thumbnail.value?.remoteId == null) { - return ''; - } - return getThumbnailCacheKeyForRemoteId( - album.thumbnail.value!.remoteId!, - album.thumbnail.value!.thumbhash!, - type: type, - ); -} - String getOriginalUrlForRemoteId(final String id, {bool edited = true}) { return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original?edited=$edited'; } diff --git a/mobile/lib/utils/immich_loading_overlay.dart b/mobile/lib/utils/immich_loading_overlay.dart deleted file mode 100644 index be49c3bae9..0000000000 --- a/mobile/lib/utils/immich_loading_overlay.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -final _loadingEntry = OverlayEntry( - builder: (context) => SizedBox.square( - dimension: double.infinity, - child: DecoratedBox( - decoration: BoxDecoration(color: context.colorScheme.surface.withAlpha(200)), - child: const Center( - child: DelayedLoadingIndicator(delay: Duration(seconds: 1), fadeInDuration: Duration(milliseconds: 400)), - ), - ), - ), -); - -ValueNotifier useProcessingOverlay() { - return use(const _LoadingOverlay()); -} - -class _LoadingOverlay extends Hook> { - const _LoadingOverlay(); - - @override - _LoadingOverlayState createState() => _LoadingOverlayState(); -} - -class _LoadingOverlayState extends HookState, _LoadingOverlay> { - late final _isLoading = ValueNotifier(false)..addListener(_listener); - OverlayEntry? _loadingOverlay; - - void _listener() { - setState(() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_isLoading.value) { - _loadingOverlay?.remove(); - _loadingOverlay = _loadingEntry; - Overlay.of(context).insert(_loadingEntry); - } else { - _loadingOverlay?.remove(); - _loadingOverlay = null; - } - }); - }); - } - - @override - ValueNotifier build(BuildContext context) { - return _isLoading; - } - - @override - void dispose() { - _isLoading.dispose(); - super.dispose(); - } - - @override - Object? get debugValue => _isLoading.value; - - @override - String get debugLabel => 'useProcessingOverlay<>'; -} diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart index c8224b9c55..20b56d4875 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -5,7 +5,6 @@ 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'; @@ -38,13 +37,9 @@ Cancelable runInIsolateGentle({ BackgroundIsolateBinaryMessenger.ensureInitialized(token); DartPluginRegistrant.ensureInitialized(); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false); + final (drift, logDb) = await Bootstrap.initDomain(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)), ], @@ -66,15 +61,6 @@ Cancelable runInIsolateGentle({ 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 { diff --git a/mobile/lib/utils/matrix.utils.dart b/mobile/lib/utils/matrix.utils.dart new file mode 100644 index 0000000000..8363a8b93d --- /dev/null +++ b/mobile/lib/utils/matrix.utils.dart @@ -0,0 +1,50 @@ +import 'dart:math'; + +class AffineMatrix { + final double a; + final double b; + final double c; + final double d; + final double e; + final double f; + + const AffineMatrix(this.a, this.b, this.c, this.d, this.e, this.f); + + @override + String toString() { + return 'AffineMatrix(a: $a, b: $b, c: $c, d: $d, e: $e, f: $f)'; + } + + factory AffineMatrix.identity() { + return const AffineMatrix(1, 0, 0, 1, 0, 0); + } + + AffineMatrix multiply(AffineMatrix other) { + return AffineMatrix( + a * other.a + c * other.b, + b * other.a + d * other.b, + a * other.c + c * other.d, + b * other.c + d * other.d, + a * other.e + c * other.f + e, + b * other.e + d * other.f + f, + ); + } + + factory AffineMatrix.compose([List transformations = const []]) { + return transformations.fold(AffineMatrix.identity(), (acc, matrix) => acc.multiply(matrix)); + } + + factory AffineMatrix.rotate(double angle) { + final cosAngle = cos(angle); + final sinAngle = sin(angle); + return AffineMatrix(cosAngle, -sinAngle, sinAngle, cosAngle, 0, 0); + } + + factory AffineMatrix.flipY() { + return const AffineMatrix(-1, 0, 0, 1, 0, 0); + } + + factory AffineMatrix.flipX() { + return const AffineMatrix(1, 0, 0, -1, 0, 0); + } +} diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 76916cee1e..9ac805af39 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -1,115 +1,14 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -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/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart' as isar_backup_album; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'; -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/infrastructure/repositories/sync_stream.repository.dart'; -import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; -import 'package:immich_mobile/platform/network_api.g.dart'; -import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/datetime_helpers.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:isar/isar.dart'; -// ignore: import_rule_photo_manager -import 'package:photo_manager/photo_manager.dart'; const int targetVersion = 25; -Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { - final hasVersion = Store.tryGet(StoreKey.version) != null; +Future migrateDatabaseIfNeeded() async { 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); - if (value != null) { - final id = value.intValue; - if (id != null) { - await db.writeTxn(() async { - final user = await db.users.get(id); - await db.storeValues.put(StoreValue(StoreKey.currentUser.id, strValue: user?.id)); - }); - } - } - } - - if (version < 10) { - await Store.put(StoreKey.version, targetVersion); - await _migrateDeviceAsset(db); - } - - if (version < 13) { - await Store.put(StoreKey.photoManagerCustomFilter, true); - } - - // This means that the SQLite DB is just created and has no version - if (version < 14 || !hasVersion) { - await migrateStoreToSqlite(db, drift); - await Store.populateCache(); - } - - final syncStreamRepository = SyncStreamRepository(drift); - await handleBetaMigration(version, await _isNewInstallation(db, drift), syncStreamRepository); - - if (version < 17 && Store.isBetaTimelineEnabled) { - final delay = Store.get(StoreKey.backupTriggerDelay, AppSettingsEnum.backupTriggerDelay.defaultValue); - if (delay >= 1000) { - await Store.put(StoreKey.backupTriggerDelay, (delay / 1000).toInt()); - } - } - - if (version < 18 && Store.isBetaTimelineEnabled) { - await syncStreamRepository.reset(); - await Store.put(StoreKey.shouldResetSync, true); - } - - if (version < 19 && Store.isBetaTimelineEnabled) { - if (!await _populateLocalAssetTime(drift)) { - return; - } - } - - if (version < 20 && Store.isBetaTimelineEnabled) { - await _syncLocalAlbumIsIosSharedAlbum(drift); - } - - if (version < 21) { - final certData = SSLClientCertStoreVal.load(); - if (certData != null) { - await networkApi.addCertificate(ClientCertData(data: certData.data, password: certData.password ?? "")); - } - } - - if (version < 23 && Store.isBetaTimelineEnabled) { - await _populateLocalAssetPlaybackStyle(drift); - } - - if (version < 24 && Store.isBetaTimelineEnabled) { - await _applyLocalAssetOrientation(drift); - } if (version < 25) { final accessToken = Store.tryGet(StoreKey.accessToken); @@ -121,365 +20,6 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { } } - if (version < 22 && !Store.isBetaTimelineEnabled) { - await Store.put(StoreKey.needBetaMigration, true); - } - - if (targetVersion >= 12) { - await Store.put(StoreKey.version, targetVersion); - return; - } - - final shouldTruncate = version < 8 || version < targetVersion; - - if (shouldTruncate) { - await _migrateTo(db, targetVersion); - } -} - -Future handleBetaMigration(int version, bool isNewInstallation, SyncStreamRepository syncStreamRepository) async { - // Handle migration only for this version - // TODO: remove when old timeline is removed - final isBeta = Store.tryGet(StoreKey.betaTimeline); - final needBetaMigration = Store.tryGet(StoreKey.needBetaMigration); - if (version <= 15 && needBetaMigration == null) { - // For new installations, no migration needed - // For existing installations, only migrate if beta timeline is not enabled (null or false) - if (isNewInstallation || isBeta == true) { - await Store.put(StoreKey.needBetaMigration, false); - await Store.put(StoreKey.betaTimeline, true); - } else { - await Store.put(StoreKey.needBetaMigration, true); - } - } - - if (version > 15) { - if (isBeta == null || isBeta) { - await Store.put(StoreKey.needBetaMigration, false); - await Store.put(StoreKey.betaTimeline, true); - } else { - await Store.put(StoreKey.needBetaMigration, false); - } - } - - if (version < 16) { - await syncStreamRepository.reset(); - await Store.put(StoreKey.shouldResetSync, true); - } -} - -Future _isNewInstallation(Isar db, Drift drift) async { - try { - final isarUserCount = await db.users.count(); - if (isarUserCount > 0) { - return false; - } - - final isarAssetCount = await db.assets.count(); - if (isarAssetCount > 0) { - return false; - } - - final driftStoreCount = await drift.storeEntity.select().get().then((list) => list.length); - if (driftStoreCount > 0) { - return false; - } - - final driftAssetCount = await drift.localAssetEntity.select().get().then((list) => list.length); - if (driftAssetCount > 0) { - return false; - } - - return true; - } catch (error) { - dPrint(() => "[MIGRATION] Error checking if new installation: $error"); - return false; - } -} - -Future _migrateTo(Isar db, int version) async { - await Store.delete(StoreKey.assetETag); - await db.writeTxn(() async { - await db.assets.clear(); - await db.exifInfos.clear(); - await db.albums.clear(); - await db.eTags.clear(); - await db.users.clear(); - }); - await Store.put(StoreKey.version, version); -} - -Future _migrateDeviceAsset(Isar db) async { - final ids = Platform.isAndroid - ? (await db.androidDeviceAssets.where().findAll()) - .map((a) => _DeviceAsset(assetId: a.id.toString(), hash: a.hash)) - .toList() - : (await db.iOSDeviceAssets.where().findAll()).map((i) => _DeviceAsset(assetId: i.id, hash: i.hash)).toList(); - - final PermissionState ps = await PhotoManager.requestPermissionExtend(); - if (!ps.hasAccess) { - dPrint(() => "[MIGRATION] Photo library permission not granted. Skipping device asset migration."); - return; - } - - List<_DeviceAsset> localAssets = []; - final List paths = await PhotoManager.getAssetPathList(onlyAll: true); - - if (paths.isEmpty) { - localAssets = (await db.assets.where().anyOf(ids, (query, id) => query.localIdEqualTo(id.assetId)).findAll()) - .map((a) => _DeviceAsset(assetId: a.localId!, dateTime: a.fileModifiedAt)) - .toList(); - } else { - final AssetPathEntity albumWithAll = paths.first; - final int assetCount = await albumWithAll.assetCountAsync; - - final List allDeviceAssets = await albumWithAll.getAssetListRange(start: 0, end: assetCount); - - localAssets = allDeviceAssets.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)).toList(); - } - - 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 = []; - await diffSortedLists( - ids, - localAssets, - compare: (a, b) => a.assetId.compareTo(b.assetId), - both: (deviceAsset, asset) { - toAdd.add( - DeviceAssetEntity(assetId: deviceAsset.assetId, hash: deviceAsset.hash!, modifiedTime: asset.dateTime!), - ); - return false; - }, - onlyFirst: (deviceAsset) { - dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}'); - }, - onlySecond: (asset) { - dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}'); - }, - ); - - dPrint(() => "[MIGRATION] Total number of device assets migrated - ${toAdd.length}"); - - await db.writeTxn(() async { - await db.deviceAssetEntitys.putAll(toAdd); - }); -} - -Future _populateLocalAssetTime(Drift db) async { - try { - final nativeApi = NativeSyncApi(); - final albums = await nativeApi.getAlbums(); - for (final album in albums) { - final assets = await nativeApi.getAssetsForAlbum(album.id); - await db.batch((batch) async { - for (final asset in assets) { - batch.update( - db.localAssetEntity, - LocalAssetEntityCompanion( - longitude: Value(asset.longitude), - latitude: Value(asset.latitude), - adjustmentTime: Value(tryFromSecondsSinceEpoch(asset.adjustmentTime, isUtc: true)), - updatedAt: Value(tryFromSecondsSinceEpoch(asset.updatedAt, isUtc: true) ?? DateTime.timestamp()), - ), - where: (t) => t.id.equals(asset.id), - ); - } - }); - } - - return true; - } catch (error) { - dPrint(() => "[MIGRATION] Error while populating asset time: $error"); - return false; - } -} - -Future _syncLocalAlbumIsIosSharedAlbum(Drift db) async { - try { - final nativeApi = NativeSyncApi(); - final albums = await nativeApi.getAlbums(); - await db.batch((batch) { - for (final album in albums) { - batch.update( - db.localAlbumEntity, - LocalAlbumEntityCompanion(isIosSharedAlbum: Value(album.isCloud)), - where: (t) => t.id.equals(album.id), - ); - } - }); - dPrint(() => "[MIGRATION] Successfully updated isIosSharedAlbum for ${albums.length} albums"); - } catch (error) { - dPrint(() => "[MIGRATION] Error while syncing local album isIosSharedAlbum: $error"); - } -} - -Future migrateDeviceAssetToSqlite(Isar db, Drift drift) async { - try { - final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll(); - await drift.batch((batch) { - for (final deviceAsset in isarDeviceAssets) { - batch.update( - drift.localAssetEntity, - LocalAssetEntityCompanion(checksum: Value(base64.encode(deviceAsset.hash))), - where: (t) => t.id.equals(deviceAsset.assetId), - ); - } - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating device assets to SQLite: $error"); - } -} - -Future migrateBackupAlbumsToSqlite(Isar db, Drift drift) async { - try { - final isarBackupAlbums = await db.backupAlbums.where().findAll(); - // Recents is a virtual album on Android, and we don't have it with the new sync - // If recents is selected previously, select all albums during migration except the excluded ones - if (Platform.isAndroid) { - final recentAlbum = isarBackupAlbums.firstWhereOrNull((album) => album.id == 'isAll'); - if (recentAlbum != null) { - await drift.localAlbumEntity.update().write( - const LocalAlbumEntityCompanion(backupSelection: Value(BackupSelection.selected)), - ); - final excluded = isarBackupAlbums - .where((album) => album.selection == isar_backup_album.BackupSelection.exclude) - .map((album) => album.id) - .toList(); - await drift.batch((batch) async { - for (final id in excluded) { - batch.update( - drift.localAlbumEntity, - const LocalAlbumEntityCompanion(backupSelection: Value(BackupSelection.excluded)), - where: (t) => t.id.equals(id), - ); - } - }); - return; - } - } - - await drift.batch((batch) { - for (final album in isarBackupAlbums) { - batch.update( - drift.localAlbumEntity, - LocalAlbumEntityCompanion( - backupSelection: Value(switch (album.selection) { - isar_backup_album.BackupSelection.none => BackupSelection.none, - isar_backup_album.BackupSelection.select => BackupSelection.selected, - isar_backup_album.BackupSelection.exclude => BackupSelection.excluded, - }), - ), - where: (t) => t.id.equals(album.id), - ); - } - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating backup albums to SQLite: $error"); - } -} - -Future migrateStoreToSqlite(Isar db, Drift drift) async { - try { - final isarStoreValues = await db.storeValues.where().findAll(); - await drift.batch((batch) { - for (final storeValue in isarStoreValues) { - final companion = StoreEntityCompanion( - id: Value(storeValue.id), - stringValue: Value(storeValue.strValue), - intValue: Value(storeValue.intValue), - ); - batch.insert(drift.storeEntity, companion, onConflict: DoUpdate((_) => companion)); - } - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating store values to SQLite: $error"); - } -} - -Future migrateStoreToIsar(Isar db, Drift drift) async { - try { - final driftStoreValues = await drift.storeEntity - .select() - .map((entity) => StoreValue(entity.id, intValue: entity.intValue, strValue: entity.stringValue)) - .get(); - - await db.writeTxn(() async { - await db.storeValues.putAll(driftStoreValues); - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating store values to Isar: $error"); - } -} - -Future _populateLocalAssetPlaybackStyle(Drift db) async { - try { - final nativeApi = NativeSyncApi(); - - final albums = await nativeApi.getAlbums(); - for (final album in albums) { - final assets = await nativeApi.getAssetsForAlbum(album.id); - await db.batch((batch) { - for (final asset in assets) { - batch.update( - db.localAssetEntity, - LocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))), - where: (t) => t.id.equals(asset.id), - ); - } - }); - } - - if (Platform.isAndroid) { - final trashedAssetMap = await nativeApi.getTrashedAssets(); - for (final entry in trashedAssetMap.cast>().entries) { - final assets = entry.value.cast(); - await db.batch((batch) { - for (final asset in assets) { - batch.update( - db.trashedLocalAssetEntity, - TrashedLocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))), - where: (t) => t.id.equals(asset.id), - ); - } - }); - } - dPrint(() => "[MIGRATION] Successfully populated playbackStyle for local and trashed assets"); - } else { - dPrint(() => "[MIGRATION] Successfully populated playbackStyle for local assets"); - } - } catch (error) { - dPrint(() => "[MIGRATION] Error while populating playbackStyle: $error"); - } -} - -Future _applyLocalAssetOrientation(Drift db) { - final query = db.localAssetEntity.update() - ..where((filter) => (filter.orientation.equals(90) | (filter.orientation.equals(270)))); - return query.write( - LocalAssetEntityCompanion.custom( - width: db.localAssetEntity.height, - height: db.localAssetEntity.width, - orientation: const Variable(0), - ), - ); -} - -AssetPlaybackStyle _toPlaybackStyle(PlatformAssetPlaybackStyle style) => switch (style) { - PlatformAssetPlaybackStyle.unknown => AssetPlaybackStyle.unknown, - PlatformAssetPlaybackStyle.image => AssetPlaybackStyle.image, - PlatformAssetPlaybackStyle.video => AssetPlaybackStyle.video, - PlatformAssetPlaybackStyle.imageAnimated => AssetPlaybackStyle.imageAnimated, - PlatformAssetPlaybackStyle.livePhoto => AssetPlaybackStyle.livePhoto, - PlatformAssetPlaybackStyle.videoLooping => AssetPlaybackStyle.videoLooping, -}; - -class _DeviceAsset { - final String assetId; - final List? hash; - final DateTime? dateTime; - - const _DeviceAsset({required this.assetId, this.hash, this.dateTime}); + await Store.put(StoreKey.version, targetVersion); + return; } diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 090889ff32..38c805a42e 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -5,13 +5,13 @@ dynamic upgradeDto(dynamic value, String targetType) { case 'UserPreferencesResponseDto': if (value is Map) { addDefault(value, 'download.includeEmbeddedVideos', false); - addDefault(value, 'folders', FoldersResponse().toJson()); - addDefault(value, 'memories', MemoriesResponse().toJson()); - addDefault(value, 'ratings', RatingsResponse().toJson()); - addDefault(value, 'people', PeopleResponse().toJson()); - addDefault(value, 'tags', TagsResponse().toJson()); - addDefault(value, 'sharedLinks', SharedLinksResponse().toJson()); - addDefault(value, 'cast', CastResponse().toJson()); + addDefault(value, 'folders', FoldersResponse(enabled: false, sidebarWeb: false).toJson()); + addDefault(value, 'memories', MemoriesResponse(enabled: true, duration: 5).toJson()); + addDefault(value, 'ratings', RatingsResponse(enabled: false).toJson()); + addDefault(value, 'people', PeopleResponse(enabled: true, sidebarWeb: false).toJson()); + addDefault(value, 'tags', TagsResponse(enabled: false, sidebarWeb: false).toJson()); + addDefault(value, 'sharedLinks', SharedLinksResponse(enabled: true, sidebarWeb: false).toJson()); + addDefault(value, 'cast', CastResponse(gCastEnabled: false).toJson()); addDefault(value, 'albums', {'defaultAssetOrder': 'desc'}); } break; diff --git a/mobile/lib/utils/provider_utils.dart b/mobile/lib/utils/provider_utils.dart index 6c2d6e0f11..9524433c05 100644 --- a/mobile/lib/utils/provider_utils.dart +++ b/mobile/lib/utils/provider_utils.dart @@ -2,21 +2,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/infrastructure/search.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/activity_api.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; import 'package:immich_mobile/repositories/person_api.repository.dart'; -import 'package:immich_mobile/repositories/timeline.repository.dart'; void invalidateAllApiRepositoryProviders(WidgetRef ref) { ref.invalidate(userApiRepositoryProvider); ref.invalidate(activityApiRepositoryProvider); ref.invalidate(partnerApiRepositoryProvider); - ref.invalidate(albumApiRepositoryProvider); ref.invalidate(personApiRepositoryProvider); ref.invalidate(assetApiRepositoryProvider); - ref.invalidate(timelineRepositoryProvider); ref.invalidate(searchApiRepositoryProvider); // Drift diff --git a/mobile/lib/utils/selection_handlers.dart b/mobile/lib/utils/selection_handlers.dart deleted file mode 100644 index f0d333e262..0000000000 --- a/mobile/lib/utils/selection_handlers.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/services/share.service.dart'; -import 'package:immich_mobile/widgets/common/date_time_picker.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/location_picker.dart'; -import 'package:immich_mobile/widgets/common/share_dialog.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -void handleShareAssets(WidgetRef ref, BuildContext context, Iterable selection) { - showDialog( - context: context, - builder: (BuildContext buildContext) { - ref.watch(shareServiceProvider).shareAssets(selection.toList(), context).then((bool status) { - if (!status) { - ImmichToast.show( - context: context, - msg: 'image_viewer_page_state_provider_share_error'.tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - buildContext.pop(); - }); - return const ShareDialog(); - }, - barrierDismissible: false, - useRootNavigator: false, - ); -} - -Future handleArchiveAssets( - WidgetRef ref, - BuildContext context, - List selection, { - bool? shouldArchive, - ToastGravity toastGravity = ToastGravity.BOTTOM, -}) async { - if (selection.isNotEmpty) { - shouldArchive ??= !selection.every((a) => a.isArchived); - await ref.read(assetProvider.notifier).toggleArchive(selection, shouldArchive); - final message = shouldArchive - ? 'moved_to_archive'.t(context: context, args: {'count': selection.length}) - : 'moved_to_library'.t(context: context, args: {'count': selection.length}); - if (context.mounted) { - ImmichToast.show(context: context, msg: message, gravity: toastGravity); - } - } -} - -Future handleFavoriteAssets( - WidgetRef ref, - BuildContext context, - List selection, { - bool? shouldFavorite, - ToastGravity toastGravity = ToastGravity.BOTTOM, -}) async { - if (selection.isNotEmpty) { - shouldFavorite ??= !selection.every((a) => a.isFavorite); - await ref.watch(assetProvider.notifier).toggleFavorite(selection, shouldFavorite); - - final assetOrAssets = selection.length > 1 ? 'assets' : 'asset'; - final toastMessage = shouldFavorite - ? 'Added ${selection.length} $assetOrAssets to favorites' - : 'Removed ${selection.length} $assetOrAssets from favorites'; - if (context.mounted) { - ImmichToast.show(context: context, msg: toastMessage, gravity: toastGravity); - } - } -} - -Future handleEditDateTime(WidgetRef ref, BuildContext context, List selection) async { - DateTime? initialDate; - String? timeZone; - Duration? offset; - if (selection.length == 1) { - final asset = selection.first; - final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset); - final (dt, oft) = assetWithExif.getTZAdjustedTimeAndOffset(); - initialDate = dt; - offset = oft; - timeZone = assetWithExif.exifInfo?.timeZone; - } - final dateTime = await showDateTimePicker( - context: context, - initialDateTime: initialDate, - initialTZ: timeZone, - initialTZOffset: offset, - ); - - if (dateTime == null) { - return; - } - - await ref.read(assetServiceProvider).changeDateTime(selection.toList(), dateTime); -} - -Future handleEditLocation(WidgetRef ref, BuildContext context, List selection) async { - LatLng? initialLatLng; - if (selection.length == 1) { - final asset = selection.first; - final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset); - if (assetWithExif.exifInfo?.latitude != null && assetWithExif.exifInfo?.longitude != null) { - initialLatLng = LatLng(assetWithExif.exifInfo!.latitude!, assetWithExif.exifInfo!.longitude!); - } - } - - final location = await showLocationPicker(context: context, initialLatLng: initialLatLng); - - if (location == null) { - return; - } - - await ref.read(assetServiceProvider).changeLocation(selection.toList(), location); -} - -Future handleSetAssetsVisibility( - WidgetRef ref, - BuildContext context, - AssetVisibilityEnum visibility, - List selection, -) async { - if (selection.isNotEmpty) { - await ref.watch(assetProvider.notifier).setLockedView(selection, visibility); - - final assetOrAssets = selection.length > 1 ? 'assets' : 'asset'; - final toastMessage = visibility == AssetVisibilityEnum.locked - ? 'Added ${selection.length} $assetOrAssets to locked folder' - : 'Removed ${selection.length} $assetOrAssets from locked folder'; - if (context.mounted) { - ImmichToast.show(context: context, msg: toastMessage, gravity: ToastGravity.BOTTOM); - } - } -} diff --git a/mobile/lib/utils/string_helper.dart b/mobile/lib/utils/string_helper.dart deleted file mode 100644 index 201d141531..0000000000 --- a/mobile/lib/utils/string_helper.dart +++ /dev/null @@ -1,7 +0,0 @@ -extension StringExtension on String { - String capitalizeFirstLetter() { - return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; - } -} - -String s(num count) => (count == 1 ? '' : 's'); diff --git a/mobile/lib/utils/throttle.dart b/mobile/lib/utils/throttle.dart deleted file mode 100644 index 8b41d92318..0000000000 --- a/mobile/lib/utils/throttle.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter_hooks/flutter_hooks.dart'; - -/// Throttles function calls with the [interval] provided. -/// Also make sures to call the last Action after the elapsed interval -class Throttler { - final Duration interval; - DateTime? _lastActionTime; - - Throttler({required this.interval}); - - T? run(T Function() action) { - if (_lastActionTime == null || (DateTime.now().difference(_lastActionTime!) > interval)) { - final response = action(); - _lastActionTime = DateTime.now(); - return response; - } - - return null; - } - - void dispose() { - _lastActionTime = null; - } -} - -/// Creates a [Throttler] that will be disposed automatically. If no [interval] is provided, a -/// default interval of 300ms is used to throttle the function calls -Throttler useThrottler({Duration interval = const Duration(milliseconds: 300), List? keys}) => - use(_ThrottleHook(interval: interval, keys: keys)); - -class _ThrottleHook extends Hook { - const _ThrottleHook({required this.interval, super.keys}); - - final Duration interval; - - @override - HookState> createState() => _ThrottlerHookState(); -} - -class _ThrottlerHookState extends HookState { - late final throttler = Throttler(interval: hook.interval); - - @override - Throttler build(_) => throttler; - - @override - void dispose() => throttler.dispose(); - - @override - String get debugLabel => 'useThrottler'; -} diff --git a/mobile/lib/utils/thumbnail_utils.dart b/mobile/lib/utils/thumbnail_utils.dart deleted file mode 100644 index 685dc2b1c2..0000000000 --- a/mobile/lib/utils/thumbnail_utils.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; - -String getAltText(ExifInfo? exifInfo, DateTime fileCreatedAt, AssetType type, List peopleNames) { - if (exifInfo?.description != null && exifInfo!.description!.isNotEmpty) { - return exifInfo.description!; - } - final (template, args) = getAltTextTemplate(exifInfo, fileCreatedAt, type, peopleNames); - return template.t(args: args); -} - -(String, Map) getAltTextTemplate( - ExifInfo? exifInfo, - DateTime fileCreatedAt, - AssetType type, - List peopleNames, -) { - final isVideo = type == AssetType.video; - final hasLocation = exifInfo?.city != null && exifInfo?.country != null; - final date = DateFormat.yMMMMd().format(fileCreatedAt); - final args = { - "isVideo": isVideo.toString(), - "date": date, - "city": exifInfo?.city ?? "", - "country": exifInfo?.country ?? "", - "person1": peopleNames.elementAtOrNull(0) ?? "", - "person2": peopleNames.elementAtOrNull(1) ?? "", - "person3": peopleNames.elementAtOrNull(2) ?? "", - "additionalCount": (peopleNames.length - 3).toString(), - }; - final template = hasLocation - ? (switch (peopleNames.length) { - 0 => "image_alt_text_date_place", - 1 => "image_alt_text_date_place_1_person", - 2 => "image_alt_text_date_place_2_people", - 3 => "image_alt_text_date_place_3_people", - _ => "image_alt_text_date_place_4_or_more_people", - }) - : (switch (peopleNames.length) { - 0 => "image_alt_text_date", - 1 => "image_alt_text_date_1_person", - 2 => "image_alt_text_date_2_people", - 3 => "image_alt_text_date_3_people", - _ => "image_alt_text_date_4_or_more_people", - }); - return (template, args); -} diff --git a/mobile/lib/utils/user_agent.dart b/mobile/lib/utils/user_agent.dart index 232bcaec38..f08793e3a1 100644 --- a/mobile/lib/utils/user_agent.dart +++ b/mobile/lib/utils/user_agent.dart @@ -1,15 +1,16 @@ import 'dart:io' show Platform; + import 'package:package_info_plus/package_info_plus.dart'; Future getUserAgentString() async { final packageInfo = await PackageInfo.fromPlatform(); String platform; if (Platform.isAndroid) { - platform = 'Android'; + platform = 'android'; } else if (Platform.isIOS) { - platform = 'iOS'; + platform = 'ios'; } else { - platform = 'Unknown'; + platform = 'unknown'; } - return 'Immich_${platform}_${packageInfo.version}'; + return 'immich-$platform/${packageInfo.version}'; } diff --git a/mobile/lib/widgets/activities/activity_text_field.dart b/mobile/lib/widgets/activities/activity_text_field.dart deleted file mode 100644 index d21cdfbc94..0000000000 --- a/mobile/lib/widgets/activities/activity_text_field.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class ActivityTextField extends HookConsumerWidget { - final bool isEnabled; - final String? likeId; - final Function(String) onSubmit; - - const ActivityTextField({required this.onSubmit, this.isEnabled = true, this.likeId, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentAlbumProvider)!; - final asset = ref.watch(currentAssetProvider); - final activityNotifier = ref.read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier); - final user = ref.watch(currentUserProvider); - final inputController = useTextEditingController(); - final inputFocusNode = useFocusNode(); - final liked = likeId != null; - - // Show keyboard immediately on activities open - useEffect(() { - inputFocusNode.requestFocus(); - return null; - }, []); - - // Pass text to callback and reset controller - void onEditingComplete() { - onSubmit(inputController.text); - inputController.clear(); - inputFocusNode.unfocus(); - } - - Future addLike() async { - await activityNotifier.addLike(); - } - - Future removeLike() async { - if (liked) { - await activityNotifier.removeActivity(likeId!); - } - } - - return Padding( - padding: const EdgeInsets.only(bottom: 10), - child: TextField( - controller: inputController, - enabled: isEnabled, - focusNode: inputFocusNode, - textInputAction: TextInputAction.send, - autofocus: false, - decoration: InputDecoration( - border: InputBorder.none, - focusedBorder: InputBorder.none, - prefixIcon: user != null - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: UserCircleAvatar(user: user, size: 30), - ) - : null, - suffixIcon: Padding( - padding: const EdgeInsets.only(right: 10), - child: IconButton( - icon: Icon(liked ? Icons.thumb_up : Icons.thumb_up_off_alt), - onPressed: liked ? removeLike : addLike, - ), - ), - suffixIconColor: liked ? context.primaryColor : null, - hintText: !isEnabled ? 'shared_album_activities_input_disable'.tr() : 'say_something'.tr(), - hintStyle: TextStyle(fontWeight: FontWeight.normal, fontSize: 14, color: Colors.grey[600]), - ), - onEditingComplete: onEditingComplete, - onTapOutside: (_) => inputFocusNode.unfocus(), - ), - ); - } -} diff --git a/mobile/lib/widgets/activities/activity_tile.dart b/mobile/lib/widgets/activities/activity_tile.dart deleted file mode 100644 index ac3b6c95a4..0000000000 --- a/mobile/lib/widgets/activities/activity_tile.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/datetime_extensions.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class ActivityTile extends HookConsumerWidget { - final Activity activity; - final bool isBottomSheet; - - const ActivityTile(this.activity, {super.key, this.isBottomSheet = false}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = ref.watch(currentAssetProvider); - final isLike = activity.type == ActivityType.like; - // Asset thumbnail is displayed when we are accessing activities from the album page - // currentAssetProvider will not be set until we open the gallery viewer - final showAssetThumbnail = asset == null && activity.assetId != null && !isBottomSheet; - - onTap() async { - final activityService = ref.read(activityServiceProvider); - final route = await activityService.buildAssetViewerRoute(activity.assetId!, ref); - if (route != null) { - await context.pushRoute(route); - } - } - - return ListTile( - minVerticalPadding: 15, - leading: isLike - ? Container( - width: isBottomSheet ? 30 : 44, - alignment: Alignment.center, - child: Icon(Icons.thumb_up, color: context.primaryColor), - ) - : isBottomSheet - ? UserCircleAvatar(user: activity.user, size: 30) - : UserCircleAvatar(user: activity.user), - title: _ActivityTitle( - userName: activity.user.name, - createdAt: activity.createdAt.timeAgo(), - leftAlign: isBottomSheet ? false : (isLike || showAssetThumbnail), - ), - // No subtitle for like, so center title - titleAlignment: !isLike ? ListTileTitleAlignment.top : ListTileTitleAlignment.center, - trailing: showAssetThumbnail ? _ActivityAssetThumbnail(activity.assetId!, onTap) : null, - subtitle: !isLike ? Text(activity.comment!) : null, - ); - } -} - -class _ActivityTitle extends StatelessWidget { - final String userName; - final String createdAt; - final bool leftAlign; - - const _ActivityTitle({required this.userName, required this.createdAt, required this.leftAlign}); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - final textStyle = context.textTheme.bodyMedium?.copyWith(color: textColor.withValues(alpha: 0.6)); - - return Row( - mainAxisAlignment: leftAlign ? MainAxisAlignment.start : MainAxisAlignment.spaceBetween, - mainAxisSize: leftAlign ? MainAxisSize.min : MainAxisSize.max, - children: [ - Text(userName, style: textStyle, overflow: TextOverflow.ellipsis), - if (leftAlign) Text(" â€ĸ ", style: textStyle), - Expanded( - child: Text( - createdAt, - style: textStyle, - overflow: TextOverflow.ellipsis, - textAlign: leftAlign ? TextAlign.left : TextAlign.right, - ), - ), - ], - ); - } -} - -class _ActivityAssetThumbnail extends StatelessWidget { - final String assetId; - final GestureTapCallback? onTap; - - const _ActivityAssetThumbnail(this.assetId, this.onTap); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 40, - height: 30, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(4)), - image: DecorationImage( - image: RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: ""), - fit: BoxFit.cover, - ), - ), - child: const SizedBox.shrink(), - ), - ); - } -} diff --git a/mobile/lib/widgets/activities/comment_bubble.dart b/mobile/lib/widgets/activities/comment_bubble.dart index 401e4b8e99..22cb0586bc 100644 --- a/mobile/lib/widgets/activities/comment_bubble.dart +++ b/mobile/lib/widgets/activities/comment_bubble.dart @@ -29,7 +29,7 @@ class CommentBubble extends ConsumerWidget { final bgColor = isOwn ? context.colorScheme.primaryContainer : context.colorScheme.surfaceContainer; final activityNotifier = ref.read( - albumActivityProvider(album.id, isAssetActivity ? activity.assetId : null).notifier, + albumActivityProvider((album.id, isAssetActivity ? activity.assetId : null)).notifier, ); Future openAssetViewer() async { diff --git a/mobile/lib/widgets/activities/dismissible_activity.dart b/mobile/lib/widgets/activities/dismissible_activity.dart index 806181ecdc..c056f5ee35 100644 --- a/mobile/lib/widgets/activities/dismissible_activity.dart +++ b/mobile/lib/widgets/activities/dismissible_activity.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -/// Wraps an [ActivityTile] and makes it dismissible class DismissibleActivity extends StatelessWidget { final String activityId; final Widget body; diff --git a/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart b/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart deleted file mode 100644 index d8f6a8885a..0000000000 --- a/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/widgets/common/drag_sheet.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AddToAlbumBottomSheet extends HookConsumerWidget { - /// The asset to add to an album - final List assets; - - const AddToAlbumBottomSheet({super.key, required this.assets}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList(); - final albumService = ref.watch(albumServiceProvider); - - useEffect(() { - // Fetch album updates, e.g., cover image - ref.read(albumProvider.notifier).refreshRemoteAlbums(); - - return null; - }, []); - - void addToAlbum(Album album) async { - final result = await albumService.addAssets(album, assets); - - if (result != null) { - if (result.alreadyInAlbum.isNotEmpty) { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}), - ); - } else { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {"album": album.name}), - ); - } - } - context.pop(); - } - - return Card( - elevation: 0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)), - ), - child: CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: SliverToBoxAdapter( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 12), - const Align(alignment: Alignment.center, child: CustomDraggingHandle()), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('add_to_album'.tr(), style: context.textTheme.displayMedium), - TextButton.icon( - icon: Icon(Icons.add, color: context.primaryColor), - label: Text('common_create_new_album'.tr(), style: TextStyle(color: context.primaryColor)), - onPressed: () { - context.pushRoute(CreateAlbumRoute(assets: assets)); - }, - ), - ], - ), - ], - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: AddToAlbumSliverList( - albums: albums, - sharedAlbums: albums.where((a) => a.shared).toList(), - onAddToAlbum: addToAlbum, - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/album/add_to_album_sliverlist.dart b/mobile/lib/widgets/album/add_to_album_sliverlist.dart deleted file mode 100644 index defbd90388..0000000000 --- a/mobile/lib/widgets/album/add_to_album_sliverlist.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/widgets/album/album_thumbnail_listtile.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; - -class AddToAlbumSliverList extends HookConsumerWidget { - /// The asset to add to an album - final List albums; - final List sharedAlbums; - final void Function(Album) onAddToAlbum; - final bool enabled; - - const AddToAlbumSliverList({ - super.key, - required this.onAddToAlbum, - required this.albums, - required this.sharedAlbums, - this.enabled = true, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumSortMode = ref.watch(albumSortByOptionsProvider); - final albumSortIsReverse = ref.watch(albumSortOrderProvider); - final sortedAlbums = albumSortMode.sortFn(albums, albumSortIsReverse); - final sortedSharedAlbums = albumSortMode.sortFn(sharedAlbums, albumSortIsReverse); - - return SliverList( - delegate: SliverChildBuilderDelegate(childCount: albums.length + (sharedAlbums.isEmpty ? 0 : 1), ( - context, - index, - ) { - // Build shared expander - if (index == 0 && sortedSharedAlbums.isNotEmpty) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: ExpansionTile( - title: Text('shared'.tr()), - tilePadding: const EdgeInsets.symmetric(horizontal: 10.0), - leading: const Icon(Icons.group), - children: [ - ListView.builder( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - itemCount: sortedSharedAlbums.length, - itemBuilder: (context, index) => AlbumThumbnailListTile( - album: sortedSharedAlbums[index], - onTap: enabled ? () => onAddToAlbum(sortedSharedAlbums[index]) : () {}, - ), - ), - ], - ), - ); - } - - // Build albums list - final offset = index - (sharedAlbums.isNotEmpty ? 1 : 0); - final album = sortedAlbums[offset]; - return AlbumThumbnailListTile(album: album, onTap: enabled ? () => onAddToAlbum(album) : () {}); - }), - ); - } -} diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart deleted file mode 100644 index 6c56f5d843..0000000000 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -class AlbumThumbnailCard extends ConsumerWidget { - final Function()? onTap; - - /// Whether or not to show the owner of the album (or "Owned") - /// in the subtitle of the album - final bool showOwner; - final bool showTitle; - - const AlbumThumbnailCard({super.key, required this.album, this.onTap, this.showOwner = false, this.showTitle = true}); - - final Album album; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return LayoutBuilder( - builder: (context, constraints) { - var cardSize = constraints.maxWidth; - - buildEmptyThumbnail() { - return Container( - height: cardSize, - width: cardSize, - decoration: BoxDecoration(color: context.colorScheme.surfaceContainerHigh), - child: Center( - child: Icon(Icons.no_photography, size: cardSize * .15, color: context.colorScheme.primary), - ), - ); - } - - buildAlbumThumbnail() => ImmichThumbnail(asset: album.thumbnail.value, width: cardSize, height: cardSize); - - buildAlbumTextRow() { - // Add the owner name to the subtitle - String? owner; - if (showOwner) { - if (album.ownerId == ref.read(currentUserProvider)?.id) { - owner = 'owned'.tr(); - } else if (album.ownerName != null) { - owner = 'shared_by_user'.t(context: context, args: {'user': album.ownerName!}); - } - } - - return Text.rich( - TextSpan( - children: [ - TextSpan( - text: 'items_count'.t(context: context, args: {'count': album.assetCount}), - ), - if (owner != null) const TextSpan(text: ' â€ĸ '), - if (owner != null) TextSpan(text: owner), - ], - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ), - overflow: TextOverflow.fade, - ); - } - - return GestureDetector( - onTap: onTap, - child: Flex( - direction: Axis.vertical, - children: [ - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: cardSize, - height: cardSize, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(20)), - child: album.thumbnail.value == null ? buildEmptyThumbnail() : buildAlbumThumbnail(), - ), - ), - if (showTitle) ...[ - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: SizedBox( - width: cardSize, - child: Text( - album.name, - overflow: TextOverflow.ellipsis, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - buildAlbumTextRow(), - ], - ], - ), - ), - ], - ), - ); - }, - ); - } -} diff --git a/mobile/lib/widgets/album/album_thumbnail_listtile.dart b/mobile/lib/widgets/album/album_thumbnail_listtile.dart deleted file mode 100644 index 386084b034..0000000000 --- a/mobile/lib/widgets/album/album_thumbnail_listtile.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:openapi/api.dart'; - -class AlbumThumbnailListTile extends StatelessWidget { - const AlbumThumbnailListTile({super.key, required this.album, this.onTap}); - - final Album album; - final void Function()? onTap; - - @override - Widget build(BuildContext context) { - var cardSize = 68.0; - - buildEmptyThumbnail() { - return Container( - decoration: BoxDecoration(color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[200]), - child: SizedBox( - height: cardSize, - width: cardSize, - child: const Center(child: Icon(Icons.no_photography)), - ), - ); - } - - buildAlbumThumbnail() { - return SizedBox( - width: cardSize, - height: cardSize, - child: Thumbnail( - imageProvider: RemoteImageProvider(url: getAlbumThumbnailUrl(album, type: AssetMediaSize.thumbnail)), - ), - ); - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: - onTap ?? - () { - context.pushRoute(AlbumViewerRoute(albumId: album.id)); - }, - child: Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: album.thumbnail.value == null ? buildEmptyThumbnail() : buildAlbumThumbnail(), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - album.name, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'items_count'.t(context: context, args: {'count': album.assetCount}), - style: const TextStyle(fontSize: 12), - ), - if (album.shared) ...[ - const Text(' â€ĸ ', style: TextStyle(fontSize: 12)), - Text('shared'.tr(), style: const TextStyle(fontSize: 12)), - ], - ], - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/album/album_title_text_field.dart b/mobile/lib/widgets/album/album_title_text_field.dart deleted file mode 100644 index 0a7438b7ae..0000000000 --- a/mobile/lib/widgets/album/album_title_text_field.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album_title.provider.dart'; - -class AlbumTitleTextField extends ConsumerWidget { - const AlbumTitleTextField({ - super.key, - required this.isAlbumTitleEmpty, - required this.albumTitleTextFieldFocusNode, - required this.albumTitleController, - required this.isAlbumTitleTextFieldFocus, - }); - - final ValueNotifier isAlbumTitleEmpty; - final FocusNode albumTitleTextFieldFocusNode; - final TextEditingController albumTitleController; - final ValueNotifier isAlbumTitleTextFieldFocus; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return TextField( - onChanged: (v) { - if (v.isEmpty) { - isAlbumTitleEmpty.value = true; - } else { - isAlbumTitleEmpty.value = false; - } - - ref.watch(albumTitleProvider.notifier).setAlbumTitle(v); - }, - focusNode: albumTitleTextFieldFocusNode, - style: TextStyle(fontSize: 28, color: context.colorScheme.onSurface, fontWeight: FontWeight.bold), - controller: albumTitleController, - onTap: () { - isAlbumTitleTextFieldFocus.value = true; - - if (albumTitleController.text == 'Untitled') { - albumTitleController.clear(); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - suffixIcon: !isAlbumTitleEmpty.value && isAlbumTitleTextFieldFocus.value - ? IconButton( - onPressed: () { - albumTitleController.clear(); - isAlbumTitleEmpty.value = true; - }, - icon: Icon(Icons.cancel_rounded, color: context.primaryColor), - splashRadius: 10, - ) - : null, - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - hintText: 'add_a_title'.tr(), - hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( - fontSize: 28, - fontWeight: FontWeight.bold, - ), - focusColor: Colors.grey[300], - fillColor: context.colorScheme.surfaceContainerHigh, - filled: isAlbumTitleTextFieldFocus.value, - ), - ); - } -} diff --git a/mobile/lib/widgets/album/album_viewer_appbar.dart b/mobile/lib/widgets/album/album_viewer_appbar.dart deleted file mode 100644 index 4fd4b31013..0000000000 --- a/mobile/lib/widgets/album/album_viewer_appbar.dart +++ /dev/null @@ -1,307 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidget { - const AlbumViewerAppbar({ - super.key, - required this.userId, - required this.titleFocusNode, - required this.descriptionFocusNode, - this.onAddPhotos, - this.onAddUsers, - required this.onActivities, - }); - - final String userId; - final FocusNode titleFocusNode; - final FocusNode descriptionFocusNode; - final void Function()? onAddPhotos; - final void Function()? onAddUsers; - final void Function() onActivities; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumState = useState(ref.read(currentAlbumProvider)); - final album = albumState.value; - ref.listen(currentAlbumProvider, (_, newAlbum) { - final oldAlbum = albumState.value; - if (oldAlbum != null && newAlbum != null && oldAlbum.id == newAlbum.id) { - return; - } - - albumState.value = newAlbum; - }); - - if (album == null) { - return const SizedBox(); - } - - final albumViewer = ref.watch(albumViewerProvider); - final newAlbumTitle = albumViewer.editTitleText; - final newAlbumDescription = albumViewer.editDescriptionText; - final isEditAlbum = albumViewer.isEditAlbum; - - final comments = album.shared ? ref.watch(activityStatisticsProvider(album.remoteId!)) : 0; - - deleteAlbum() async { - final bool success = await ref.watch(albumProvider.notifier).deleteAlbum(album); - - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - - if (!success) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_delete".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - } - - Future onDeleteAlbumPressed() { - return showDialog( - context: context, - barrierDismissible: false, // user must tap button! - builder: (BuildContext context) { - return AlertDialog( - title: const Text('delete_album').tr(), - content: const Text('album_viewer_appbar_delete_confirm').tr(), - actions: [ - TextButton( - onPressed: () => context.pop('Cancel'), - child: Text( - 'cancel', - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), - ), - TextButton( - onPressed: () { - context.pop('Confirm'); - deleteAlbum(); - }, - child: Text( - 'confirm', - style: TextStyle(fontWeight: FontWeight.bold, color: context.colorScheme.error), - ).tr(), - ), - ], - ); - }, - ); - } - - void onLeaveAlbumPressed() async { - bool isSuccess = await ref.watch(albumProvider.notifier).leaveAlbum(album); - - if (isSuccess) { - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - } else { - context.pop(); - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_leave".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - } - - buildBottomSheetActions() { - return [ - album.ownerId == userId - ? ListTile( - leading: const Icon(Icons.delete_forever_rounded), - title: const Text('delete_album', style: TextStyle(fontWeight: FontWeight.w500)).tr(), - onTap: onDeleteAlbumPressed, - ) - : ListTile( - leading: const Icon(Icons.person_remove_rounded), - title: const Text( - 'album_viewer_appbar_share_leave', - style: TextStyle(fontWeight: FontWeight.w500), - ).tr(), - onTap: onLeaveAlbumPressed, - ), - ]; - // } - } - - void onSortOrderToggled() async { - final updatedAlbum = await ref.read(albumProvider.notifier).toggleSortOrder(album); - - if (updatedAlbum == null) { - ImmichToast.show( - context: context, - msg: "error_change_sort_album".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - - context.pop(); - } - - void buildBottomSheet() { - final ownerActions = [ - ListTile( - leading: const Icon(Icons.person_add_alt_rounded), - onTap: () { - context.pop(); - final onAddUsers = this.onAddUsers; - if (onAddUsers != null) { - onAddUsers(); - } - }, - title: const Text("album_viewer_page_share_add_users", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ListTile( - leading: const Icon(Icons.swap_vert_rounded), - onTap: onSortOrderToggled, - title: const Text("change_display_order", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ListTile( - leading: const Icon(Icons.link_rounded), - onTap: () { - context.pushRoute(SharedLinkEditRoute(albumId: album.remoteId)); - context.pop(); - }, - title: const Text("control_bottom_app_bar_share_link", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ListTile( - leading: const Icon(Icons.settings_rounded), - onTap: () => context.navigateTo(const AlbumOptionsRoute()), - title: const Text("options", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ]; - - final commonActions = [ - ListTile( - leading: const Icon(Icons.add_photo_alternate_outlined), - onTap: () { - context.pop(); - final onAddPhotos = this.onAddPhotos; - if (onAddPhotos != null) { - onAddPhotos(); - } - }, - title: const Text("add_photos", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ]; - showModalBottomSheet( - backgroundColor: context.scaffoldBackgroundColor, - isScrollControlled: false, - context: context, - builder: (context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: ListView( - shrinkWrap: true, - children: [ - ...buildBottomSheetActions(), - if (onAddPhotos != null) ...commonActions, - if (onAddPhotos != null && userId == album.ownerId) ...ownerActions, - ], - ), - ), - ); - }, - ); - } - - Widget buildActivitiesButton() { - return IconButton( - onPressed: onActivities, - icon: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(Icons.mode_comment_outlined), - if (comments != 0) - Padding( - padding: const EdgeInsets.only(left: 5), - child: Text( - comments.toString(), - style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), - ), - ), - ], - ), - ); - } - - buildLeadingButton() { - if (isEditAlbum) { - return IconButton( - onPressed: () async { - if (newAlbumTitle.isNotEmpty) { - bool isSuccess = await ref.watch(albumViewerProvider.notifier).changeAlbumTitle(album, newAlbumTitle); - if (!isSuccess) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_title".tr(), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - titleFocusNode.unfocus(); - } else if (newAlbumDescription.isNotEmpty) { - bool isSuccessDescription = await ref - .watch(albumViewerProvider.notifier) - .changeAlbumDescription(album, newAlbumDescription); - if (!isSuccessDescription) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_description".tr(), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - descriptionFocusNode.unfocus(); - } else { - titleFocusNode.unfocus(); - descriptionFocusNode.unfocus(); - ref.read(albumViewerProvider.notifier).disableEditAlbum(); - } - }, - icon: const Icon(Icons.check_rounded), - splashRadius: 25, - ); - } else { - return IconButton( - onPressed: context.maybePop, - icon: const Icon(Icons.arrow_back_ios_rounded), - splashRadius: 25, - ); - } - } - - return AppBar( - elevation: 0, - backgroundColor: context.scaffoldBackgroundColor, - leading: buildLeadingButton(), - centerTitle: false, - actions: [ - if (album.shared && (album.activityEnabled || comments != 0)) buildActivitiesButton(), - if (album.isRemote) ...[ - IconButton(splashRadius: 25, onPressed: buildBottomSheet, icon: const Icon(Icons.more_horiz_rounded)), - ], - ], - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} diff --git a/mobile/lib/widgets/album/album_viewer_editable_description.dart b/mobile/lib/widgets/album/album_viewer_editable_description.dart deleted file mode 100644 index decd268ff3..0000000000 --- a/mobile/lib/widgets/album/album_viewer_editable_description.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; - -class AlbumViewerEditableDescription extends HookConsumerWidget { - final String albumDescription; - final FocusNode descriptionFocusNode; - const AlbumViewerEditableDescription({super.key, required this.albumDescription, required this.descriptionFocusNode}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumViewerState = ref.watch(albumViewerProvider); - - final descriptionTextEditController = useTextEditingController( - text: albumViewerState.isEditAlbum && albumViewerState.editDescriptionText.isNotEmpty - ? albumViewerState.editDescriptionText - : albumDescription, - ); - - void onFocusModeChange() { - if (!descriptionFocusNode.hasFocus && descriptionTextEditController.text.isEmpty) { - ref.watch(albumViewerProvider.notifier).setEditDescriptionText(""); - descriptionTextEditController.text = ""; - } - } - - useEffect(() { - descriptionFocusNode.addListener(onFocusModeChange); - return () { - descriptionFocusNode.removeListener(onFocusModeChange); - }; - }, []); - - return Material( - color: Colors.transparent, - child: TextField( - onChanged: (value) { - if (value.isEmpty) { - } else { - ref.watch(albumViewerProvider.notifier).setEditDescriptionText(value); - } - }, - focusNode: descriptionFocusNode, - style: context.textTheme.bodyLarge, - maxLines: 3, - minLines: 1, - controller: descriptionTextEditController, - onTap: () { - context.focusScope.requestFocus(descriptionFocusNode); - - ref.watch(albumViewerProvider.notifier).setEditDescriptionText(albumDescription); - ref.watch(albumViewerProvider.notifier).enableEditAlbum(); - - if (descriptionTextEditController.text == '') { - descriptionTextEditController.clear(); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.all(8), - suffixIcon: descriptionFocusNode.hasFocus - ? IconButton( - onPressed: () { - descriptionTextEditController.clear(); - }, - icon: Icon(Icons.cancel_rounded, color: context.primaryColor), - splashRadius: 10, - ) - : null, - enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusColor: Colors.grey[300], - fillColor: context.scaffoldBackgroundColor, - filled: descriptionFocusNode.hasFocus, - hintText: 'add_a_description'.tr(), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/album/album_viewer_editable_title.dart b/mobile/lib/widgets/album/album_viewer_editable_title.dart deleted file mode 100644 index c84e613017..0000000000 --- a/mobile/lib/widgets/album/album_viewer_editable_title.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; - -class AlbumViewerEditableTitle extends HookConsumerWidget { - final String albumName; - final FocusNode titleFocusNode; - const AlbumViewerEditableTitle({super.key, required this.albumName, required this.titleFocusNode}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumViewerState = ref.watch(albumViewerProvider); - - final titleTextEditController = useTextEditingController( - text: albumViewerState.isEditAlbum && albumViewerState.editTitleText.isNotEmpty - ? albumViewerState.editTitleText - : albumName, - ); - - void onFocusModeChange() { - if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) { - ref.watch(albumViewerProvider.notifier).setEditTitleText("Untitled"); - titleTextEditController.text = "Untitled"; - } - } - - useEffect(() { - titleFocusNode.addListener(onFocusModeChange); - return () { - titleFocusNode.removeListener(onFocusModeChange); - }; - }, []); - - return Material( - color: Colors.transparent, - child: TextField( - onChanged: (value) { - if (value.isEmpty) { - } else { - ref.watch(albumViewerProvider.notifier).setEditTitleText(value); - } - }, - focusNode: titleFocusNode, - style: context.textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w700), - controller: titleTextEditController, - onTap: () { - context.focusScope.requestFocus(titleFocusNode); - - ref.watch(albumViewerProvider.notifier).setEditTitleText(albumName); - ref.watch(albumViewerProvider.notifier).enableEditAlbum(); - - if (titleTextEditController.text == 'Untitled') { - titleTextEditController.clear(); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), - suffixIcon: titleFocusNode.hasFocus - ? IconButton( - onPressed: () { - titleTextEditController.clear(); - }, - icon: Icon(Icons.cancel_rounded, color: context.primaryColor), - splashRadius: 10, - ) - : null, - enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusColor: Colors.grey[300], - fillColor: context.scaffoldBackgroundColor, - filled: titleFocusNode.hasFocus, - hintText: 'add_a_title'.tr(), - hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(fontSize: 28), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/album/shared_album_thumbnail_image.dart b/mobile/lib/widgets/album/shared_album_thumbnail_image.dart deleted file mode 100644 index b21e86d145..0000000000 --- a/mobile/lib/widgets/album/shared_album_thumbnail_image.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -class SharedAlbumThumbnailImage extends HookConsumerWidget { - final Asset asset; - - const SharedAlbumThumbnailImage({super.key, required this.asset}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return GestureDetector( - onTap: () { - // debugPrint("View ${asset.id}"); - }, - child: Stack(children: [ImmichThumbnail(asset: asset, width: 500, height: 500)]), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/asset_drag_region.dart b/mobile/lib/widgets/asset_grid/asset_drag_region.dart deleted file mode 100644 index 71e55acbd6..0000000000 --- a/mobile/lib/widgets/asset_grid/asset_drag_region.dart +++ /dev/null @@ -1,207 +0,0 @@ -// Based on https://stackoverflow.com/a/52625182 - -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; - -class AssetDragRegion extends StatefulWidget { - final Widget child; - - final void Function(AssetIndex valueKey)? onStart; - final void Function(AssetIndex valueKey)? onAssetEnter; - final void Function()? onEnd; - final void Function()? onScrollStart; - final void Function(ScrollDirection direction)? onScroll; - - const AssetDragRegion({ - super.key, - required this.child, - this.onStart, - this.onAssetEnter, - this.onEnd, - this.onScrollStart, - this.onScroll, - }); - @override - State createState() => _AssetDragRegionState(); -} - -class _AssetDragRegionState extends State { - late AssetIndex? assetUnderPointer; - late AssetIndex? anchorAsset; - - // Scroll related state - static const double scrollOffset = 0.10; - double? topScrollOffset; - double? bottomScrollOffset; - Timer? scrollTimer; - late bool scrollNotified; - - @override - void initState() { - super.initState(); - assetUnderPointer = null; - anchorAsset = null; - scrollNotified = false; - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - topScrollOffset = null; - bottomScrollOffset = null; - } - - @override - void dispose() { - scrollTimer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return RawGestureDetector( - gestures: { - _CustomLongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<_CustomLongPressGestureRecognizer>( - () => _CustomLongPressGestureRecognizer(), - _registerCallbacks, - ), - }, - child: widget.child, - ); - } - - void _registerCallbacks(_CustomLongPressGestureRecognizer recognizer) { - recognizer.onLongPressMoveUpdate = (details) => _onLongPressMove(details); - recognizer.onLongPressStart = (details) => _onLongPressStart(details); - recognizer.onLongPressUp = _onLongPressEnd; - } - - AssetIndex? _getValueKeyAtPositon(Offset position) { - final box = context.findAncestorRenderObjectOfType(); - if (box == null) return null; - - final hitTestResult = BoxHitTestResult(); - final local = box.globalToLocal(position); - if (!box.hitTest(hitTestResult, position: local)) return null; - - return (hitTestResult.path.firstWhereOrNull((hit) => hit.target is _AssetIndexProxy)?.target as _AssetIndexProxy?) - ?.index; - } - - void _onLongPressStart(LongPressStartDetails event) { - /// Calculate widget height and scroll offset when long press starting instead of in [initState] - /// or [didChangeDependencies] as the grid might still be rendering into view to get the actual size - final height = context.size?.height; - if (height != null && (topScrollOffset == null || bottomScrollOffset == null)) { - topScrollOffset = height * scrollOffset; - bottomScrollOffset = height - topScrollOffset!; - } - - final initialHit = _getValueKeyAtPositon(event.globalPosition); - anchorAsset = initialHit; - if (initialHit == null) return; - - if (anchorAsset != null) { - widget.onStart?.call(anchorAsset!); - } - } - - void _onLongPressEnd() { - scrollNotified = false; - scrollTimer?.cancel(); - widget.onEnd?.call(); - } - - void _onLongPressMove(LongPressMoveUpdateDetails event) { - if (anchorAsset == null) return; - if (topScrollOffset == null || bottomScrollOffset == null) return; - - final currentDy = event.localPosition.dy; - - if (currentDy > bottomScrollOffset!) { - scrollTimer ??= Timer.periodic( - const Duration(milliseconds: 50), - (_) => widget.onScroll?.call(ScrollDirection.forward), - ); - } else if (currentDy < topScrollOffset!) { - scrollTimer ??= Timer.periodic( - const Duration(milliseconds: 50), - (_) => widget.onScroll?.call(ScrollDirection.reverse), - ); - } else { - scrollTimer?.cancel(); - scrollTimer = null; - } - - final currentlyTouchingAsset = _getValueKeyAtPositon(event.globalPosition); - if (currentlyTouchingAsset == null) return; - - if (assetUnderPointer != currentlyTouchingAsset) { - if (!scrollNotified) { - scrollNotified = true; - widget.onScrollStart?.call(); - } - - widget.onAssetEnter?.call(currentlyTouchingAsset); - assetUnderPointer = currentlyTouchingAsset; - } - } -} - -class _CustomLongPressGestureRecognizer extends LongPressGestureRecognizer { - @override - void rejectGesture(int pointer) { - acceptGesture(pointer); - } -} - -class AssetIndexWrapper extends SingleChildRenderObjectWidget { - final int rowIndex; - final int sectionIndex; - - const AssetIndexWrapper({required Widget super.child, required this.rowIndex, required this.sectionIndex, super.key}); - - @override - // ignore: library_private_types_in_public_api - _AssetIndexProxy createRenderObject(BuildContext context) { - return _AssetIndexProxy( - index: AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex), - ); - } - - @override - void updateRenderObject( - BuildContext context, - // ignore: library_private_types_in_public_api - _AssetIndexProxy renderObject, - ) { - renderObject.index = AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex); - } -} - -class _AssetIndexProxy extends RenderProxyBox { - AssetIndex index; - - _AssetIndexProxy({required this.index}); -} - -class AssetIndex { - final int rowIndex; - final int sectionIndex; - - const AssetIndex({required this.rowIndex, required this.sectionIndex}); - - @override - bool operator ==(covariant AssetIndex other) { - if (identical(this, other)) return true; - - return other.rowIndex == rowIndex && other.sectionIndex == sectionIndex; - } - - @override - int get hashCode => rowIndex.hashCode ^ sectionIndex.hashCode; -} diff --git a/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart b/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart deleted file mode 100644 index d95d6efe2e..0000000000 --- a/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart +++ /dev/null @@ -1,307 +0,0 @@ -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:isar/isar.dart'; -import 'package:logging/logging.dart'; - -final log = Logger('AssetGridDataStructure'); - -enum RenderAssetGridElementType { assets, assetRow, groupDividerTitle, monthTitle } - -class RenderAssetGridElement { - final RenderAssetGridElementType type; - final String? title; - final DateTime date; - final int count; - final int offset; - final int totalCount; - - const RenderAssetGridElement( - this.type, { - this.title, - required this.date, - this.count = 0, - this.offset = 0, - this.totalCount = 0, - }); -} - -enum GroupAssetsBy { day, month, auto, none } - -class RenderList { - final List elements; - final List? allAssets; - final QueryBuilder? query; - final int totalAssets; - - /// reference to batch of assets loaded from DB with offset [_bufOffset] - List _buf = []; - - /// global offset of assets in [_buf] - int _bufOffset = 0; - - RenderList(this.elements, this.query, this.allAssets) : totalAssets = allAssets?.length ?? query!.countSync(); - - bool get isEmpty => totalAssets == 0; - - /// Loads the requested assets from the database to an internal buffer if not cached - /// and returns a slice of that buffer - List loadAssets(int offset, int count) { - assert(offset >= 0); - assert(count > 0); - assert(offset + count <= totalAssets); - if (allAssets != null) { - // if we already loaded all assets (e.g. from search result) - // simply return the requested slice of that array - return allAssets!.slice(offset, offset + count); - } else if (query != null) { - // general case: we have the query to load assets via offset from the DB on demand - if (offset < _bufOffset || offset + count > _bufOffset + _buf.length) { - // the requested slice (offset:offset+count) is not contained in the cache buffer `_buf` - // thus, fill the buffer with a new batch of assets that at least contains the requested - // assets and some more - - final bool forward = _bufOffset < offset; - // if the requested offset is greater than the cached offset, the user scrolls forward "down" - const batchSize = 256; - const oppositeSize = 64; - - // make sure to load a meaningful amount of data (and not only the requested slice) - // otherwise, each call to [loadAssets] would result in DB call trashing performance - // fills small requests to [batchSize], adds some legroom into the opposite scroll direction for large requests - final len = max(batchSize, count + oppositeSize); - // when scrolling forward, start shortly before the requested offset... - // when scrolling backward, end shortly after the requested offset... - // ... to guard against the user scrolling in the other direction - // a tiny bit resulting in a another required load from the DB - final start = max(0, forward ? offset - oppositeSize : (len > batchSize ? offset : offset + count - len)); - // load the calculated batch (start:start+len) from the DB and put it into the buffer - _buf = query!.offset(start).limit(len).findAllSync(); - _bufOffset = start; - } - assert(_bufOffset <= offset); - assert(_bufOffset + _buf.length >= offset + count); - // return the requested slice from the buffer (we made sure before that the assets are loaded!) - return _buf.slice(offset - _bufOffset, offset - _bufOffset + count); - } - throw Exception("RenderList has neither assets nor query"); - } - - /// Returns the requested asset either from cached buffer or directly from the database - Asset loadAsset(int index) { - if (allAssets != null) { - // all assets are already loaded (e.g. from search result) - return allAssets![index]; - } else if (query != null) { - // general case: we have the DB query to load asset(s) on demand - if (index >= _bufOffset && index < _bufOffset + _buf.length) { - // lucky case: the requested asset is already cached in the buffer! - return _buf[index - _bufOffset]; - } - // request the asset from the database (not changing the buffer!) - final asset = query!.offset(index).findFirstSync(); - if (asset == null) { - throw Exception("Asset at index $index does no longer exist in database"); - } - return asset; - } - throw Exception("RenderList has neither assets nor query"); - } - - static Future fromQuery(QueryBuilder query, GroupAssetsBy groupBy) => - _buildRenderList(null, query, groupBy); - - static Future _buildRenderList( - List? assets, - QueryBuilder? query, - GroupAssetsBy groupBy, - ) async { - final List elements = []; - - const pageSize = 50000; - const sectionSize = 60; // divides evenly by 2,3,4,5,6 - - if (groupBy == GroupAssetsBy.none) { - final int total = assets?.length ?? query!.countSync(); - - final dateLoader = query != null ? DateBatchLoader(query: query, batchSize: 1000 * sectionSize) : null; - - for (int i = 0; i < total; i += sectionSize) { - final date = assets != null ? assets[i].fileCreatedAt : await dateLoader?.getDate(i); - - final int count = i + sectionSize > total ? total - i : sectionSize; - if (date == null) break; - elements.add( - RenderAssetGridElement( - RenderAssetGridElementType.assets, - date: date, - count: count, - totalCount: total, - offset: i, - ), - ); - } - return RenderList(elements, query, assets); - } - - final formatSameYear = groupBy == GroupAssetsBy.month ? DateFormat.MMMM() : DateFormat.MMMEd(); - final formatOtherYear = groupBy == GroupAssetsBy.month ? DateFormat.yMMMM() : DateFormat.yMMMEd(); - final currentYear = DateTime.now().year; - final formatMergedSameYear = DateFormat.MMMd(); - final formatMergedOtherYear = DateFormat.yMMMd(); - - int offset = 0; - DateTime? last; - DateTime? current; - int lastOffset = 0; - int count = 0; - int monthCount = 0; - int lastMonthIndex = 0; - - String formatDateRange(DateTime from, DateTime to) { - final startDate = (from.year == currentYear ? formatMergedSameYear : formatMergedOtherYear).format(from); - final endDate = (to.year == currentYear ? formatMergedSameYear : formatMergedOtherYear).format(to); - if (DateTime(from.year, from.month, from.day) == DateTime(to.year, to.month, to.day)) { - // format range with time when both dates are on the same day - final startTime = DateFormat.Hm().format(from); - final endTime = DateFormat.Hm().format(to); - return "$startDate $startTime - $endTime"; - } - return "$startDate - $endDate"; - } - - void mergeMonth() { - if (last != null && groupBy == GroupAssetsBy.auto && monthCount <= 30 && elements.length > lastMonthIndex + 1) { - // merge all days into a single section - assert(elements[lastMonthIndex].date.month == last.month); - final e = elements[lastMonthIndex]; - - elements[lastMonthIndex] = RenderAssetGridElement( - RenderAssetGridElementType.monthTitle, - date: e.date, - count: monthCount, - totalCount: monthCount, - offset: e.offset, - title: formatDateRange(e.date, elements.last.date), - ); - elements.removeRange(lastMonthIndex + 1, elements.length); - } - } - - void addElems(DateTime d, DateTime? prevDate) { - final bool newMonth = last == null || last.year != d.year || last.month != d.month; - if (newMonth) { - mergeMonth(); - lastMonthIndex = elements.length; - monthCount = 0; - } - for (int j = 0; j < count; j += sectionSize) { - final type = j == 0 - ? (groupBy != GroupAssetsBy.month && newMonth - ? RenderAssetGridElementType.monthTitle - : RenderAssetGridElementType.groupDividerTitle) - : (groupBy == GroupAssetsBy.auto - ? RenderAssetGridElementType.groupDividerTitle - : RenderAssetGridElementType.assets); - final sectionCount = j + sectionSize > count ? count - j : sectionSize; - assert(sectionCount > 0 && sectionCount <= sectionSize); - elements.add( - RenderAssetGridElement( - type, - date: d, - count: sectionCount, - totalCount: groupBy == GroupAssetsBy.auto ? sectionCount : count, - offset: lastOffset + j, - title: j == 0 - ? (d.year == currentYear ? formatSameYear.format(d) : formatOtherYear.format(d)) - : (groupBy == GroupAssetsBy.auto ? formatDateRange(d, prevDate ?? d) : null), - ), - ); - } - monthCount += count; - } - - DateTime? prevDate; - while (true) { - // this iterates all assets (only their createdAt property) in batches - // memory usage is okay, however runtime is linear with number of assets - // TODO replace with groupBy once Isar supports such queries - final dates = assets != null - ? assets.map((a) => a.fileCreatedAt) - : await query!.offset(offset).limit(pageSize).fileCreatedAtProperty().findAll(); - int i = 0; - for (final date in dates) { - final d = DateTime(date.year, date.month, groupBy == GroupAssetsBy.month ? 1 : date.day); - current ??= d; - if (current != d) { - addElems(current, prevDate); - last = current; - current = d; - lastOffset = offset + i; - count = 0; - } - prevDate = date; - count++; - i++; - } - - if (assets != null || dates.length != pageSize) break; - offset += pageSize; - } - if (count > 0 && current != null) { - addElems(current, prevDate); - mergeMonth(); - } - assert(elements.every((e) => e.count <= sectionSize), "too large section"); - return RenderList(elements, query, assets); - } - - static RenderList empty() => RenderList([], null, []); - - static Future fromAssets(List assets, GroupAssetsBy groupBy) => - _buildRenderList(assets, null, groupBy); - - /// Deletes an asset from the render list and clears the buffer - /// This is only a workaround for deleted images still appearing in the gallery - void deleteAsset(Asset deleteAsset) { - allAssets?.remove(deleteAsset); - _buf.clear(); - _bufOffset = 0; - } -} - -class DateBatchLoader { - final QueryBuilder query; - final int batchSize; - - List _buffer = []; - int _bufferStart = 0; - - DateBatchLoader({required this.query, required this.batchSize}); - - Future getDate(int index) async { - if (!_isIndexInBuffer(index)) { - await _loadBatch(index); - } - - if (_isIndexInBuffer(index)) { - return _buffer[index - _bufferStart]; - } - - return null; - } - - Future _loadBatch(int targetIndex) async { - final batchStart = (targetIndex ~/ batchSize) * batchSize; - - _buffer = await query.offset(batchStart).limit(batchSize).fileCreatedAtProperty().findAll(); - - _bufferStart = batchStart; - } - - bool _isIndexInBuffer(int index) { - return index >= _bufferStart && index < _bufferStart + _buffer.length; - } -} diff --git a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart deleted file mode 100644 index cd2dc70dae..0000000000 --- a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart +++ /dev/null @@ -1,388 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/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(); - -class ControlBottomAppBarNotifier with ChangeNotifier { - void minimize() { - notifyListeners(); - } -} - -class ControlBottomAppBar extends HookConsumerWidget { - final void Function(bool shareLocal) onShare; - final void Function()? onFavorite; - final void Function()? onArchive; - final void Function([bool force])? onDelete; - final void Function([bool force])? onDeleteServer; - final void Function(bool onlyBackedUp)? onDeleteLocal; - final Function(Album album) onAddToAlbum; - final void Function() onCreateNewAlbum; - final void Function() onUpload; - final void Function()? onStack; - final void Function()? onEditTime; - final void Function()? onEditLocation; - final void Function()? onRemoveFromAlbum; - final void Function()? onToggleLocked; - final void Function()? onDownload; - - final bool enabled; - final bool unfavorite; - final bool unarchive; - final AssetSelectionState selectionAssetState; - final List selectedAssets; - - const ControlBottomAppBar({ - super.key, - required this.onShare, - this.onFavorite, - this.onArchive, - this.onDelete, - this.onDeleteServer, - this.onDeleteLocal, - required this.onAddToAlbum, - required this.onCreateNewAlbum, - required this.onUpload, - this.onDownload, - this.onStack, - this.onEditTime, - this.onEditLocation, - this.onRemoveFromAlbum, - this.onToggleLocked, - this.selectionAssetState = const AssetSelectionState(), - this.selectedAssets = const [], - this.enabled = true, - this.unarchive = false, - this.unfavorite = false, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final hasRemote = selectionAssetState.hasRemote || selectionAssetState.hasMerged; - final hasLocal = selectionAssetState.hasLocal || selectionAssetState.hasMerged; - final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList(); - final sharedAlbums = ref.watch(albumProvider).where((a) => a.shared).toList(); - const bottomPadding = 0.24; - final scrollController = useDraggableScrollController(); - final isInLockedView = ref.watch(inLockedViewProvider); - - void minimize() { - scrollController.animateTo(bottomPadding, duration: const Duration(milliseconds: 300), curve: Curves.easeOut); - } - - useEffect(() { - controlBottomAppBarNotifier.addListener(minimize); - return () { - controlBottomAppBarNotifier.removeListener(minimize); - }; - }, []); - - void showForceDeleteDialog(Function(bool) deleteCb, {String? alertMsg}) { - showDialog( - context: context, - builder: (BuildContext context) { - return DeleteDialog(alert: alertMsg, onDelete: () => deleteCb(true)); - }, - ); - } - - /// 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); - return; - } - return showForceDeleteDialog(deleteCb, alertMsg: alertMsg); - } - - List renderActionButtons() { - return [ - ControlBoxButton( - iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, - label: "share".tr(), - onPressed: enabled ? () => onShare(true) : null, - ), - if (!isInLockedView && hasRemote) - ControlBoxButton( - iconData: Icons.link_rounded, - 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, - label: (unarchive ? "unarchive" : "archive").tr(), - onPressed: enabled ? onArchive : null, - ), - if (hasRemote && onFavorite != null) - ControlBoxButton( - iconData: unfavorite ? Icons.favorite_border_rounded : Icons.favorite_rounded, - label: (unfavorite ? "unfavorite" : "favorite").tr(), - onPressed: enabled ? onFavorite : null, - ), - if (hasRemote && onDownload != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton(iconData: Icons.download, label: "download".tr(), onPressed: onDownload), - ), - if (hasLocal && hasRemote && onDelete != null && !isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.delete_sweep_outlined, - label: "delete".tr(), - onPressed: enabled ? () => handleRemoteDelete(!trashEnabled, onDelete!) : null, - onLongPressed: enabled ? () => showForceDeleteDialog(onDelete!) : null, - ), - ), - if (hasRemote && onDeleteServer != null && !isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 85), - child: ControlBoxButton( - iconData: Icons.cloud_off_outlined, - label: trashEnabled - ? "control_bottom_app_bar_trash_from_immich".tr() - : "control_bottom_app_bar_delete_from_immich".tr(), - onPressed: enabled - ? () => handleRemoteDelete(!trashEnabled, onDeleteServer!, alertMsg: "delete_dialog_alert_remote") - : null, - onLongPressed: enabled - ? () => showForceDeleteDialog(onDeleteServer!, alertMsg: "delete_dialog_alert_remote") - : null, - ), - ), - if (isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 110), - child: ControlBoxButton( - iconData: Icons.delete_forever, - label: "delete_dialog_title".tr(), - onPressed: enabled - ? () => showForceDeleteDialog(onDeleteServer!, alertMsg: "delete_dialog_alert_remote") - : null, - ), - ), - if (hasLocal && onDeleteLocal != null && !isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 95), - child: ControlBoxButton( - iconData: Icons.no_cell_outlined, - label: "control_bottom_app_bar_delete_from_local".tr(), - onPressed: enabled - ? () { - if (!selectionAssetState.hasLocal) { - return onDeleteLocal?.call(true); - } - - showDialog( - context: context, - builder: (BuildContext context) { - return DeleteLocalOnlyDialog(onDeleteLocal: onDeleteLocal!); - }, - ); - } - : null, - ), - ), - if (hasRemote && onEditTime != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 95), - child: ControlBoxButton( - iconData: Icons.edit_calendar_outlined, - label: "control_bottom_app_bar_edit_time".tr(), - onPressed: enabled ? onEditTime : null, - ), - ), - if (hasRemote && onEditLocation != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.edit_location_alt_outlined, - label: "control_bottom_app_bar_edit_location".tr(), - onPressed: enabled ? onEditLocation : null, - ), - ), - if (hasRemote) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 100), - child: ControlBoxButton( - iconData: isInLockedView ? Icons.lock_open_rounded : Icons.lock_outline_rounded, - label: isInLockedView ? "remove_from_locked_folder".tr() : "move_to_locked_folder".tr(), - onPressed: enabled ? onToggleLocked : null, - ), - ), - if (!selectionAssetState.hasLocal && selectionAssetState.selectedCount > 1 && onStack != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.filter_none_rounded, - label: "stack".tr(), - onPressed: enabled ? onStack : null, - ), - ), - if (onRemoveFromAlbum != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.remove_circle_outline, - label: 'remove_from_album'.tr(), - onPressed: enabled ? onRemoveFromAlbum : null, - ), - ), - if (selectionAssetState.hasLocal) - ControlBoxButton( - iconData: Icons.backup_outlined, - label: "upload".tr(), - onPressed: enabled - ? () => showDialog( - context: context, - builder: (BuildContext context) { - return UploadDialog(onUpload: onUpload); - }, - ) - : null, - ), - ]; - } - - getInitialSize() { - if (isInLockedView) { - return bottomPadding; - } - if (hasRemote) { - return 0.35; - } - return bottomPadding; - } - - getMaxChildSize() { - if (isInLockedView) { - return bottomPadding; - } - if (hasRemote) { - return 0.65; - } - return bottomPadding; - } - - return DraggableScrollableSheet( - initialChildSize: getInitialSize(), - minChildSize: bottomPadding, - maxChildSize: getMaxChildSize(), - snap: true, - controller: scrollController, - builder: (BuildContext context, ScrollController scrollController) { - return Card( - color: context.colorScheme.surfaceContainerHigh, - surfaceTintColor: context.colorScheme.surfaceContainerHigh, - elevation: 6.0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)), - ), - margin: const EdgeInsets.all(0), - child: CustomScrollView( - controller: scrollController, - slivers: [ - SliverToBoxAdapter( - child: Column( - children: [ - const SizedBox(height: 12), - const CustomDraggingHandle(), - const SizedBox(height: 12), - SizedBox( - height: 120, - child: ListView( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - children: renderActionButtons(), - ), - ), - if (hasRemote && !isInLockedView) ...[ - const Divider(indent: 16, endIndent: 16, thickness: 1), - _AddToAlbumTitleRow(onCreateNewAlbum: enabled ? onCreateNewAlbum : null), - ], - ], - ), - ), - if (hasRemote && !isInLockedView) - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: AddToAlbumSliverList( - albums: albums, - sharedAlbums: sharedAlbums, - onAddToAlbum: onAddToAlbum, - enabled: enabled, - ), - ), - ], - ), - ); - }, - ); - } -} - -class _AddToAlbumTitleRow extends StatelessWidget { - const _AddToAlbumTitleRow({required this.onCreateNewAlbum}); - - final VoidCallback? onCreateNewAlbum; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("add_to_album", style: context.textTheme.titleSmall).tr(), - TextButton.icon( - onPressed: onCreateNewAlbum, - icon: Icon(Icons.add, color: context.primaryColor), - label: Text( - "common_create_new_album", - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14), - ).tr(), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/delete_dialog.dart b/mobile/lib/widgets/asset_grid/delete_dialog.dart index adb22889a8..ff5aac617a 100644 --- a/mobile/lib/widgets/asset_grid/delete_dialog.dart +++ b/mobile/lib/widgets/asset_grid/delete_dialog.dart @@ -1,18 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; - -class DeleteDialog extends ConfirmDialog { - const DeleteDialog({super.key, String? alert, required Function onDelete}) - : super( - title: "delete_dialog_title", - content: alert ?? "delete_dialog_alert", - cancel: "cancel", - ok: "delete", - onOk: onDelete, - ); -} class DeleteLocalOnlyDialog extends StatelessWidget { final void Function(bool onlyMerged) onDeleteLocal; diff --git a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart b/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart deleted file mode 100644 index 93a1d53f4e..0000000000 --- a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class DisableMultiSelectButton extends ConsumerWidget { - const DisableMultiSelectButton({super.key, required this.onPressed, required this.selectedItemCount}); - - final Function onPressed; - final int selectedItemCount; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.only(left: 16.0, top: 8.0), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: ElevatedButton.icon( - onPressed: () => onPressed(), - icon: Icon(Icons.close_rounded, color: context.colorScheme.onPrimary), - label: Text( - '$selectedItemCount', - style: context.textTheme.titleMedium?.copyWith(height: 2.5, color: context.colorScheme.onPrimary), - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart b/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart deleted file mode 100644 index 3de52c2816..0000000000 --- a/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart +++ /dev/null @@ -1,559 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -/// Build the Scroll Thumb and label using the current configuration -typedef ScrollThumbBuilder = - Widget Function( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }); - -/// Build a Text widget using the current scroll offset -typedef LabelTextBuilder = Text Function(double offsetY); - -/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged -/// for quick navigation of the BoxScrollView. -class DraggableScrollbar extends StatefulWidget { - /// The view that will be scrolled with the scroll thumb - final CustomScrollView child; - - /// A function that builds a thumb using the current configuration - final ScrollThumbBuilder scrollThumbBuilder; - - /// The height of the scroll thumb - final double heightScrollThumb; - - /// The background color of the label and thumb - final Color backgroundColor; - - /// The amount of padding that should surround the thumb - final EdgeInsetsGeometry? padding; - - /// Determines how quickly the scrollbar will animate in and out - final Duration scrollbarAnimationDuration; - - /// How long should the thumb be visible before fading out - final Duration scrollbarTimeToFade; - - /// Build a Text widget from the current offset in the BoxScrollView - final LabelTextBuilder? labelTextBuilder; - - /// Determines box constraints for Container displaying label - final BoxConstraints? labelConstraints; - - /// The ScrollController for the BoxScrollView - final ScrollController controller; - - /// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder] - final bool alwaysVisibleScrollThumb; - - DraggableScrollbar({ - super.key, - this.alwaysVisibleScrollThumb = false, - required this.heightScrollThumb, - required this.backgroundColor, - required this.scrollThumbBuilder, - required this.child, - required this.controller, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical); - - DraggableScrollbar.rrect({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbRRectBuilder(alwaysVisibleScrollThumb); - - DraggableScrollbar.arrows({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbArrowBuilder(alwaysVisibleScrollThumb); - - DraggableScrollbar.semicircle({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb); - - @override - DraggableScrollbarState createState() => DraggableScrollbarState(); - - static buildScrollThumbAndLabel({ - required Widget scrollThumb, - required Color backgroundColor, - required Animation? thumbAnimation, - required Animation? labelAnimation, - required Text? labelText, - required BoxConstraints? labelConstraints, - required bool alwaysVisibleScrollThumb, - }) { - var scrollThumbAndLabel = labelText == null - ? scrollThumb - : Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ScrollLabel( - animation: labelAnimation, - backgroundColor: backgroundColor, - constraints: labelConstraints, - child: labelText, - ), - scrollThumb, - ], - ); - - if (alwaysVisibleScrollThumb) { - return scrollThumbAndLabel; - } - return SlideFadeTransition(animation: thumbAnimation!, child: scrollThumbAndLabel); - } - - static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = CustomPaint( - key: scrollThumbKey, - foregroundPainter: ArrowCustomPainter(Colors.white), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(height), - bottomLeft: Radius.circular(height), - topRight: const Radius.circular(4.0), - bottomRight: const Radius.circular(4.0), - ), - child: Container(constraints: BoxConstraints.tight(Size(width, height))), - ), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } - - static ScrollThumbBuilder _thumbArrowBuilder(bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = ClipPath( - clipper: const ArrowClipper(), - child: Container( - height: height, - width: 20.0, - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(12.0)), - ), - ), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } - - static ScrollThumbBuilder _thumbRRectBuilder(bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(7.0)), - child: Container(constraints: BoxConstraints.tight(Size(16.0, height))), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } -} - -class ScrollLabel extends StatelessWidget { - final Animation? animation; - final Color backgroundColor; - final Text child; - - final BoxConstraints? constraints; - static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0); - - const ScrollLabel({ - super.key, - required this.child, - required this.animation, - required this.backgroundColor, - this.constraints = _defaultConstraints, - }); - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: animation!, - child: Container( - margin: const EdgeInsets.only(right: 12.0), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(16.0)), - child: Container(constraints: constraints ?? _defaultConstraints, alignment: Alignment.center, child: child), - ), - ), - ); - } -} - -class DraggableScrollbarState extends State with TickerProviderStateMixin { - late double _barOffset; - late double _viewOffset; - late bool _isDragInProcess; - - late AnimationController _thumbAnimationController; - late Animation _thumbAnimation; - late AnimationController _labelAnimationController; - late Animation _labelAnimation; - Timer? _fadeoutTimer; - - @override - void initState() { - super.initState(); - _barOffset = 0.0; - _viewOffset = 0.0; - _isDragInProcess = false; - - _thumbAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastOutSlowIn); - - _labelAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn); - } - - @override - void dispose() { - _thumbAnimationController.dispose(); - _labelAnimationController.dispose(); - _fadeoutTimer?.cancel(); - super.dispose(); - } - - double get barMaxScrollExtent => context.size!.height - widget.heightScrollThumb; - - double get barMinScrollExtent => 0; - - double get viewMaxScrollExtent => widget.controller.position.maxScrollExtent; - - double get viewMinScrollExtent => widget.controller.position.minScrollExtent; - - @override - Widget build(BuildContext context) { - Text? labelText; - if (widget.labelTextBuilder != null && _isDragInProcess) { - labelText = widget.labelTextBuilder!(_viewOffset + _barOffset + widget.heightScrollThumb / 2); - } - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - //print("LayoutBuilder constraints=$constraints"); - - return NotificationListener( - onNotification: (ScrollNotification notification) { - changePosition(notification); - return false; - }, - child: Stack( - children: [ - RepaintBoundary(child: widget.child), - RepaintBoundary( - child: GestureDetector( - onVerticalDragStart: _onVerticalDragStart, - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - child: Container( - alignment: Alignment.topRight, - margin: EdgeInsets.only(top: _barOffset), - padding: widget.padding, - child: widget.scrollThumbBuilder( - widget.backgroundColor, - _thumbAnimation, - _labelAnimation, - widget.heightScrollThumb, - labelText: labelText, - labelConstraints: widget.labelConstraints, - ), - ), - ), - ), - ], - ), - ); - }, - ); - } - - //scroll bar has received notification that it's view was scrolled - //so it should also changes his position - //but only if it isn't dragged - changePosition(ScrollNotification notification) { - if (_isDragInProcess) { - return; - } - - setState(() { - if (notification is ScrollUpdateNotification) { - _barOffset += getBarDelta(notification.scrollDelta!, barMaxScrollExtent, viewMaxScrollExtent); - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - - _viewOffset += notification.scrollDelta!; - if (_viewOffset < widget.controller.position.minScrollExtent) { - _viewOffset = widget.controller.position.minScrollExtent; - } - if (_viewOffset > viewMaxScrollExtent) { - _viewOffset = viewMaxScrollExtent; - } - } - - if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - - _fadeoutTimer?.cancel(); - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - } - }); - } - - double getBarDelta(double scrollViewDelta, double barMaxScrollExtent, double viewMaxScrollExtent) { - return scrollViewDelta * barMaxScrollExtent / viewMaxScrollExtent; - } - - double getScrollViewDelta(double barDelta, double barMaxScrollExtent, double viewMaxScrollExtent) { - return barDelta * viewMaxScrollExtent / barMaxScrollExtent; - } - - void _onVerticalDragStart(DragStartDetails details) { - setState(() { - _isDragInProcess = true; - _labelAnimationController.forward(); - _fadeoutTimer?.cancel(); - }); - } - - void _onVerticalDragUpdate(DragUpdateDetails details) { - setState(() { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - if (_isDragInProcess) { - _barOffset += details.delta.dy; - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - - double viewDelta = getScrollViewDelta(details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent); - - _viewOffset = widget.controller.position.pixels + viewDelta; - if (_viewOffset < widget.controller.position.minScrollExtent) { - _viewOffset = widget.controller.position.minScrollExtent; - } - if (_viewOffset > viewMaxScrollExtent) { - _viewOffset = viewMaxScrollExtent; - } - widget.controller.jumpTo(_viewOffset); - } - }); - } - - void _onVerticalDragEnd(DragEndDetails details) { - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - setState(() { - _isDragInProcess = false; - }); - } -} - -/// Draws 2 triangles like arrow up and arrow down -class ArrowCustomPainter extends CustomPainter { - Color color; - - ArrowCustomPainter(this.color); - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint()..color = color; - const width = 12.0; - const height = 8.0; - final baseX = size.width / 2; - final baseY = size.height / 2; - - canvas.drawPath(_trianglePath(Offset(baseX, baseY - 2.0), width, height, true), paint); - canvas.drawPath(_trianglePath(Offset(baseX, baseY + 2.0), width, height, false), paint); - } - - static Path _trianglePath(Offset o, double width, double height, bool isUp) { - return Path() - ..moveTo(o.dx, o.dy) - ..lineTo(o.dx + width, o.dy) - ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height) - ..close(); - } -} - -///This cut 2 lines in arrow shape -class ArrowClipper extends CustomClipper { - const ArrowClipper(); - @override - Path getClip(Size size) { - Path path = Path(); - path.lineTo(0.0, size.height); - path.lineTo(size.width, size.height); - path.lineTo(size.width, 0.0); - path.lineTo(0.0, 0.0); - path.close(); - - double arrowWidth = 8.0; - double startPointX = (size.width - arrowWidth) / 2; - double startPointY = size.height / 2 - arrowWidth / 2; - path.moveTo(startPointX, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2); - path.lineTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth, startPointY + 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0); - path.lineTo(startPointX, startPointY + 1.0); - path.close(); - - startPointY = size.height / 2 + arrowWidth / 2; - path.moveTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2); - path.lineTo(startPointX, startPointY); - path.lineTo(startPointX, startPointY - 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0); - path.lineTo(startPointX + arrowWidth, startPointY - 1.0); - path.close(); - - return path; - } - - @override - bool shouldReclip(CustomClipper oldClipper) => false; -} - -class SlideFadeTransition extends StatelessWidget { - final Animation animation; - final Widget child; - - const SlideFadeTransition({super.key, required this.animation, required this.child}); - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: animation, - builder: (context, child) => animation.value == 0.0 ? const SizedBox() : child!, - child: SlideTransition( - position: Tween(begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0)).animate(animation), - child: FadeTransition(opacity: animation, child: child), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart b/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart deleted file mode 100644 index 17f35311f0..0000000000 --- a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart +++ /dev/null @@ -1,490 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -/// Build the Scroll Thumb and label using the current configuration -typedef ScrollThumbBuilder = - Widget Function( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }); - -/// Build a Text widget using the current scroll offset -typedef LabelTextBuilder = Text Function(int item); - -/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged -/// for quick navigation of the BoxScrollView. -class DraggableScrollbar extends StatefulWidget { - /// The view that will be scrolled with the scroll thumb - final ScrollablePositionedList child; - - final ItemPositionsListener itemPositionsListener; - - /// A function that builds a thumb using the current configuration - final ScrollThumbBuilder scrollThumbBuilder; - - /// The height of the scroll thumb - final double heightScrollThumb; - - /// The background color of the label and thumb - final Color backgroundColor; - - /// The amount of padding that should surround the thumb - final EdgeInsetsGeometry? padding; - - /// The height offset of the thumb/bar from the bottom of the page - final double? heightOffset; - - /// Determines how quickly the scrollbar will animate in and out - final Duration scrollbarAnimationDuration; - - /// How long should the thumb be visible before fading out - final Duration scrollbarTimeToFade; - - /// Build a Text widget from the current offset in the BoxScrollView - final LabelTextBuilder? labelTextBuilder; - - /// Determines box constraints for Container displaying label - final BoxConstraints? labelConstraints; - - /// The ScrollController for the BoxScrollView - final ItemScrollController controller; - - /// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder] - final bool alwaysVisibleScrollThumb; - - final Function(bool scrolling) scrollStateListener; - - DraggableScrollbar.semicircle({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - required this.itemPositionsListener, - required this.scrollStateListener, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.heightOffset, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb); - - @override - DraggableScrollbarState createState() => DraggableScrollbarState(); - - static buildScrollThumbAndLabel({ - required Widget scrollThumb, - required Color backgroundColor, - required Animation? thumbAnimation, - required Animation? labelAnimation, - required Text? labelText, - required BoxConstraints? labelConstraints, - required bool alwaysVisibleScrollThumb, - }) { - var scrollThumbAndLabel = labelText == null - ? scrollThumb - : Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ScrollLabel( - animation: labelAnimation, - backgroundColor: backgroundColor, - constraints: labelConstraints, - child: labelText, - ), - scrollThumb, - ], - ); - - if (alwaysVisibleScrollThumb) { - return scrollThumbAndLabel; - } - return SlideFadeTransition(animation: thumbAnimation!, child: scrollThumbAndLabel); - } - - static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = CustomPaint( - key: scrollThumbKey, - foregroundPainter: ArrowCustomPainter(Colors.white), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(height), - bottomLeft: Radius.circular(height), - topRight: const Radius.circular(4.0), - bottomRight: const Radius.circular(4.0), - ), - child: Container(constraints: BoxConstraints.tight(Size(width, height))), - ), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } -} - -class ScrollLabel extends StatelessWidget { - final Animation? animation; - final Color backgroundColor; - final Text child; - - final BoxConstraints? constraints; - static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0); - - const ScrollLabel({ - super.key, - required this.child, - required this.animation, - required this.backgroundColor, - this.constraints = _defaultConstraints, - }); - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: animation!, - child: Container( - margin: const EdgeInsets.only(right: 12.0), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(16.0)), - child: Container( - constraints: constraints ?? _defaultConstraints, - padding: const EdgeInsets.symmetric(horizontal: 10.0), - alignment: Alignment.center, - child: child, - ), - ), - ), - ); - } -} - -class DraggableScrollbarState extends State with TickerProviderStateMixin { - late double _barOffset; - late bool _isDragInProcess; - late int _currentItem; - - late AnimationController _thumbAnimationController; - late Animation _thumbAnimation; - late AnimationController _labelAnimationController; - late Animation _labelAnimation; - Timer? _fadeoutTimer; - - @override - void initState() { - super.initState(); - _barOffset = 0.0; - _isDragInProcess = false; - _currentItem = 0; - - _thumbAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastOutSlowIn); - - _labelAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn); - } - - @override - void dispose() { - _thumbAnimationController.dispose(); - _labelAnimationController.dispose(); - _fadeoutTimer?.cancel(); - super.dispose(); - } - - double get barMaxScrollExtent => (context.size?.height ?? 0) - widget.heightScrollThumb - (widget.heightOffset ?? 0); - - double get barMinScrollExtent => 0; - - int get maxItemCount => widget.child.itemCount; - - @override - Widget build(BuildContext context) { - Text? labelText; - if (widget.labelTextBuilder != null && _isDragInProcess) { - labelText = widget.labelTextBuilder!(_currentItem); - } - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - //print("LayoutBuilder constraints=$constraints"); - - return NotificationListener( - onNotification: (ScrollNotification notification) { - changePosition(notification); - return false; - }, - child: Stack( - children: [ - RepaintBoundary(child: widget.child), - RepaintBoundary( - child: GestureDetector( - onVerticalDragStart: _onVerticalDragStart, - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - child: Container( - alignment: Alignment.topRight, - margin: EdgeInsets.only(top: _barOffset), - padding: widget.padding, - child: widget.scrollThumbBuilder( - widget.backgroundColor, - _thumbAnimation, - _labelAnimation, - widget.heightScrollThumb, - labelText: labelText, - labelConstraints: widget.labelConstraints, - ), - ), - ), - ), - ], - ), - ); - }, - ); - } - - // scroll bar has received notification that it's view was scrolled - // so it should also changes his position - // but only if it isn't dragged - changePosition(ScrollNotification notification) { - if (_isDragInProcess) { - return; - } - - setState(() { - try { - int firstItemIndex = widget.itemPositionsListener.itemPositions.value.first.index; - - if (notification is ScrollUpdateNotification) { - _barOffset = (firstItemIndex / maxItemCount) * barMaxScrollExtent; - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - } - - if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - - if (itemPosition < maxItemCount) { - _currentItem = itemPosition; - } - - _fadeoutTimer?.cancel(); - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - } - } catch (_) {} - }); - } - - void _onVerticalDragStart(DragStartDetails details) { - setState(() { - _isDragInProcess = true; - _labelAnimationController.forward(); - _fadeoutTimer?.cancel(); - }); - - widget.scrollStateListener(true); - } - - int get itemPosition { - int numberOfItems = widget.child.itemCount; - return ((_barOffset / barMaxScrollExtent) * numberOfItems).toInt(); - } - - void _jumpToBarPosition() { - if (itemPosition > maxItemCount - 1) { - return; - } - - _currentItem = itemPosition; - - /// If the bar is at the bottom but the item position is still smaller than the max item count (due to rounding error) - /// jump to the end of the list - if (barMaxScrollExtent - _barOffset < 10 && itemPosition < maxItemCount) { - widget.controller.jumpTo(index: maxItemCount); - - return; - } - - widget.controller.jumpTo(index: itemPosition); - } - - Timer? dragHaltTimer; - int lastTimerPosition = 0; - - void _onVerticalDragUpdate(DragUpdateDetails details) { - setState(() { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - if (_isDragInProcess) { - _barOffset += details.delta.dy; - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - - if (itemPosition != lastTimerPosition) { - lastTimerPosition = itemPosition; - dragHaltTimer?.cancel(); - widget.scrollStateListener(true); - - dragHaltTimer = Timer(const Duration(milliseconds: 500), () { - widget.scrollStateListener(false); - }); - } - - _jumpToBarPosition(); - } - }); - } - - void _onVerticalDragEnd(DragEndDetails details) { - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - - setState(() { - _jumpToBarPosition(); - _isDragInProcess = false; - }); - - widget.scrollStateListener(false); - } -} - -/// Draws 2 triangles like arrow up and arrow down -class ArrowCustomPainter extends CustomPainter { - Color color; - - ArrowCustomPainter(this.color); - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint()..color = color; - const width = 12.0; - const height = 8.0; - final baseX = size.width / 2; - final baseY = size.height / 2; - - canvas.drawPath(_trianglePath(Offset(baseX, baseY - 2.0), width, height, true), paint); - canvas.drawPath(_trianglePath(Offset(baseX, baseY + 2.0), width, height, false), paint); - } - - static Path _trianglePath(Offset o, double width, double height, bool isUp) { - return Path() - ..moveTo(o.dx, o.dy) - ..lineTo(o.dx + width, o.dy) - ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height) - ..close(); - } -} - -///This cut 2 lines in arrow shape -class ArrowClipper extends CustomClipper { - const ArrowClipper(); - @override - Path getClip(Size size) { - Path path = Path(); - path.lineTo(0.0, size.height); - path.lineTo(size.width, size.height); - path.lineTo(size.width, 0.0); - path.lineTo(0.0, 0.0); - path.close(); - - double arrowWidth = 8.0; - double startPointX = (size.width - arrowWidth) / 2; - double startPointY = size.height / 2 - arrowWidth / 2; - path.moveTo(startPointX, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2); - path.lineTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth, startPointY + 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0); - path.lineTo(startPointX, startPointY + 1.0); - path.close(); - - startPointY = size.height / 2 + arrowWidth / 2; - path.moveTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2); - path.lineTo(startPointX, startPointY); - path.lineTo(startPointX, startPointY - 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0); - path.lineTo(startPointX + arrowWidth, startPointY - 1.0); - path.close(); - - return path; - } - - @override - bool shouldReclip(CustomClipper oldClipper) => false; -} - -class SlideFadeTransition extends StatelessWidget { - final Animation animation; - final Widget child; - - const SlideFadeTransition({super.key, required this.animation, required this.child}); - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: animation, - builder: (context, child) => animation.value == 0.0 ? const SizedBox() : child!, - child: SlideTransition( - position: Tween(begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0)).animate(animation), - child: FadeTransition(opacity: animation, child: child), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/group_divider_title.dart b/mobile/lib/widgets/asset_grid/group_divider_title.dart deleted file mode 100644 index 1464c941f0..0000000000 --- a/mobile/lib/widgets/asset_grid/group_divider_title.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; - -class GroupDividerTitle extends HookConsumerWidget { - const GroupDividerTitle({ - super.key, - required this.text, - required this.multiselectEnabled, - required this.onSelect, - required this.onDeselect, - required this.selected, - }); - - final String text; - final bool multiselectEnabled; - final Function onSelect; - final Function onDeselect; - final bool selected; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final appSettingService = ref.watch(appSettingsServiceProvider); - final groupBy = useState(GroupAssetsBy.day); - - useEffect(() { - groupBy.value = GroupAssetsBy.values[appSettingService.getSetting(AppSettingsEnum.groupAssetsBy)]; - return null; - }, []); - - void handleTitleIconClick() { - ref.read(hapticFeedbackProvider.notifier).heavyImpact(); - if (selected) { - onDeselect(); - } else { - onSelect(); - } - } - - return Padding( - padding: EdgeInsets.only( - top: groupBy.value == GroupAssetsBy.month ? 32.0 : 16.0, - bottom: 16.0, - left: 12.0, - right: 12.0, - ), - child: Row( - children: [ - Text( - text, - style: groupBy.value == GroupAssetsBy.month - ? context.textTheme.bodyLarge?.copyWith(fontSize: 24.0) - : context.textTheme.labelLarge?.copyWith( - color: context.textTheme.labelLarge?.color?.withAlpha(250), - fontWeight: FontWeight.w500, - ), - ), - const Spacer(), - GestureDetector( - onTap: handleTitleIconClick, - child: multiselectEnabled && selected - ? Icon( - Icons.check_circle_rounded, - color: context.primaryColor, - semanticLabel: "unselect_all_in".tr(namedArgs: {"group": text}), - ) - : Icon( - Icons.check_circle_outline_rounded, - color: context.colorScheme.onSurfaceSecondary, - semanticLabel: "select_all_in".tr(namedArgs: {"group": text}), - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid.dart deleted file mode 100644 index ab6b350a7b..0000000000 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/gestures.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/asyncvalue_extensions.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid_view.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -class ImmichAssetGrid extends HookConsumerWidget { - final int? assetsPerRow; - final double margin; - final bool? showStorageIndicator; - final ImmichAssetGridSelectionListener? listener; - final bool selectionActive; - final List? assets; - final RenderList? renderList; - final Future Function()? onRefresh; - final Set? preselectedAssets; - final bool canDeselect; - final bool? dynamicLayout; - final bool showMultiSelectIndicator; - final void Function(Iterable itemPositions)? visibleItemsListener; - final Widget? topWidget; - final bool shrinkWrap; - final bool showDragScroll; - final bool showDragScrollLabel; - final bool showStack; - - const ImmichAssetGrid({ - super.key, - this.assets, - this.onRefresh, - this.renderList, - this.assetsPerRow, - this.showStorageIndicator, - this.listener, - this.margin = 2.0, - this.selectionActive = false, - this.preselectedAssets, - this.canDeselect = true, - this.dynamicLayout, - this.showMultiSelectIndicator = true, - this.visibleItemsListener, - this.topWidget, - this.shrinkWrap = false, - this.showDragScroll = true, - this.showDragScrollLabel = true, - this.showStack = false, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - var settings = ref.watch(appSettingsServiceProvider); - - final perRow = useState(assetsPerRow ?? settings.getSetting(AppSettingsEnum.tilesPerRow)!); - final scaleFactor = useState(7.0 - perRow.value); - final baseScaleFactor = useState(7.0 - perRow.value); - - /// assets need different hero tags across tabs / modals - /// otherwise, hero animations are performed across tabs (looks buggy!) - int heroOffset() { - const int range = 1152921504606846976; // 2^60 - final tabScope = TabsRouterScope.of(context); - if (tabScope != null) { - final int tabIndex = tabScope.controller.activeIndex; - return tabIndex * range; - } - return range * 7; - } - - Widget buildAssetGridView(RenderList renderList) { - return RawGestureDetector( - gestures: { - CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => CustomScaleGestureRecognizer(), - (CustomScaleGestureRecognizer scale) { - scale.onStart = (details) { - baseScaleFactor.value = scaleFactor.value; - }; - - scale.onUpdate = (details) { - scaleFactor.value = max(min(5.0, baseScaleFactor.value * details.scale), 1.0); - if (7 - scaleFactor.value.toInt() != perRow.value) { - perRow.value = 7 - scaleFactor.value.toInt(); - settings.setSetting(AppSettingsEnum.tilesPerRow, perRow.value); - } - }; - }, - ), - }, - child: ImmichAssetGridView( - onRefresh: onRefresh, - assetsPerRow: perRow.value, - listener: listener, - showStorageIndicator: showStorageIndicator ?? settings.getSetting(AppSettingsEnum.storageIndicator), - renderList: renderList, - margin: margin, - selectionActive: selectionActive, - preselectedAssets: preselectedAssets, - canDeselect: canDeselect, - dynamicLayout: dynamicLayout ?? settings.getSetting(AppSettingsEnum.dynamicLayout), - showMultiSelectIndicator: showMultiSelectIndicator, - visibleItemsListener: visibleItemsListener, - topWidget: topWidget, - heroOffset: heroOffset(), - shrinkWrap: shrinkWrap, - showDragScroll: showDragScroll, - showStack: showStack, - showLabel: showDragScrollLabel, - ), - ); - } - - if (renderList != null) return buildAssetGridView(renderList!); - - final renderListFuture = ref.watch(assetsTimelineProvider(assets!)); - return renderListFuture.widgetWhen(onData: (renderList) => buildAssetGridView(renderList)); - } -} - -/// accepts a gesture even though it should reject it (because child won) -class CustomScaleGestureRecognizer extends ScaleGestureRecognizer { - @override - void rejectGesture(int pointer) { - acceptGesture(pointer); - } -} diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart deleted file mode 100644 index c323c573b4..0000000000 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart +++ /dev/null @@ -1,828 +0,0 @@ -import 'dart:collection'; -import 'dart:developer'; -import 'dart:math'; - -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:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart'; -import 'package:immich_mobile/widgets/asset_grid/disable_multi_select_button.dart'; -import 'package:immich_mobile/widgets/asset_grid/draggable_scrollbar_custom.dart'; -import 'package:immich_mobile/widgets/asset_grid/group_divider_title.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -typedef ImmichAssetGridSelectionListener = void Function(bool, Set); - -class ImmichAssetGridView extends ConsumerStatefulWidget { - final RenderList renderList; - final int assetsPerRow; - final double margin; - final bool showStorageIndicator; - final ImmichAssetGridSelectionListener? listener; - final bool selectionActive; - final Future Function()? onRefresh; - final Set? preselectedAssets; - final bool canDeselect; - final bool dynamicLayout; - final bool showMultiSelectIndicator; - final void Function(Iterable itemPositions)? visibleItemsListener; - final Widget? topWidget; - final int heroOffset; - final bool shrinkWrap; - final bool showDragScroll; - final bool showStack; - final bool showLabel; - - const ImmichAssetGridView({ - super.key, - required this.renderList, - required this.assetsPerRow, - required this.showStorageIndicator, - this.listener, - this.margin = 5.0, - this.selectionActive = false, - this.onRefresh, - this.preselectedAssets, - this.canDeselect = true, - this.dynamicLayout = true, - this.showMultiSelectIndicator = true, - this.visibleItemsListener, - this.topWidget, - this.heroOffset = 0, - this.shrinkWrap = false, - this.showDragScroll = true, - this.showStack = false, - this.showLabel = true, - }); - - @override - createState() { - return ImmichAssetGridViewState(); - } -} - -class ImmichAssetGridViewState extends ConsumerState { - final ItemScrollController _itemScrollController = ItemScrollController(); - final ScrollOffsetController _scrollOffsetController = ScrollOffsetController(); - final ItemPositionsListener _itemPositionsListener = ItemPositionsListener.create(); - late final KeepAliveLink currentAssetLink; - - /// The timestamp when the haptic feedback was last invoked - int _hapticFeedbackTS = 0; - DateTime? _prevItemTime; - bool _scrolling = false; - final Set _selectedAssets = LinkedHashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id); - - bool _dragging = false; - int? _dragAnchorAssetIndex; - int? _dragAnchorSectionIndex; - final Set _draggedAssets = HashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id); - - ScrollPhysics? _scrollPhysics; - - Set _getSelectedAssets() { - return Set.from(_selectedAssets); - } - - void _callSelectionListener(bool selectionActive) { - widget.listener?.call(selectionActive, _getSelectedAssets()); - } - - void _selectAssets(List assets) { - setState(() { - if (_dragging) { - _draggedAssets.addAll(assets); - } - _selectedAssets.addAll(assets); - _callSelectionListener(true); - }); - } - - void _deselectAssets(List assets) { - final assetsToDeselect = assets.where( - (a) => widget.canDeselect || !(widget.preselectedAssets?.contains(a) ?? false), - ); - - setState(() { - _selectedAssets.removeAll(assetsToDeselect); - if (_dragging) { - _draggedAssets.removeAll(assetsToDeselect); - } - _callSelectionListener(_selectedAssets.isNotEmpty); - }); - } - - void _deselectAll() { - setState(() { - _selectedAssets.clear(); - _dragAnchorAssetIndex = null; - _dragAnchorSectionIndex = null; - _draggedAssets.clear(); - _dragging = false; - if (!widget.canDeselect && widget.preselectedAssets != null && widget.preselectedAssets!.isNotEmpty) { - _selectedAssets.addAll(widget.preselectedAssets!); - } - _callSelectionListener(false); - }); - } - - bool _allAssetsSelected(List assets) { - return widget.selectionActive && assets.firstWhereOrNull((e) => !_selectedAssets.contains(e)) == null; - } - - Future _scrollToIndex(int index) async { - // if the index is so far down, that the end of the list is reached on the screen - // the scroll_position widget crashes. This is a workaround to prevent this. - // If the index is within the last 10 elements, we jump instead of scrolling. - if (widget.renderList.elements.length <= index + 10) { - _itemScrollController.jumpTo(index: index); - return; - } - await _itemScrollController.scrollTo(index: index, alignment: 0, duration: const Duration(milliseconds: 500)); - } - - Widget _itemBuilder(BuildContext c, int position) { - int index = position; - if (widget.topWidget != null) { - if (index == 0) { - return widget.topWidget!; - } - index--; - } - - final section = widget.renderList.elements[index]; - return _Section( - showStorageIndicator: widget.showStorageIndicator, - selectedAssets: _selectedAssets, - selectionActive: widget.selectionActive, - sectionIndex: index, - section: section, - margin: widget.margin, - renderList: widget.renderList, - assetsPerRow: widget.assetsPerRow, - scrolling: _scrolling, - dynamicLayout: widget.dynamicLayout, - selectAssets: _selectAssets, - deselectAssets: _deselectAssets, - allAssetsSelected: _allAssetsSelected, - showStack: widget.showStack, - heroOffset: widget.heroOffset, - onAssetTap: (asset) { - ref.read(currentAssetProvider.notifier).set(asset); - ref.read(isPlayingMotionVideoProvider.notifier).playing = false; - if (asset.isVideo) { - ref.read(showControlsProvider.notifier).show = false; - } - }, - ); - } - - Text _labelBuilder(int pos) { - final maxLength = widget.renderList.elements.length; - if (pos < 0 || pos >= maxLength) { - return const Text(""); - } - - final date = widget.renderList.elements[pos % maxLength].date; - - return Text( - DateFormat.yMMMM().format(date), - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), - ); - } - - Widget _buildMultiSelectIndicator() { - return DisableMultiSelectButton(onPressed: () => _deselectAll(), selectedItemCount: _selectedAssets.length); - } - - Widget _buildAssetGrid() { - final useDragScrolling = widget.showDragScroll && widget.renderList.totalAssets >= 20; - - void dragScrolling(bool active) { - if (active != _scrolling) { - setState(() { - _scrolling = active; - }); - } - } - - bool appBarOffset() { - return (ref.watch(tabProvider).index == 0 && ModalRoute.of(context)?.settings.name == TabControllerRoute.name) || - (ModalRoute.of(context)?.settings.name == AlbumViewerRoute.name); - } - - final listWidget = ScrollablePositionedList.builder( - padding: EdgeInsets.only(top: appBarOffset() ? 60 : 0, bottom: 220), - itemBuilder: _itemBuilder, - itemPositionsListener: _itemPositionsListener, - physics: _scrollPhysics, - itemScrollController: _itemScrollController, - scrollOffsetController: _scrollOffsetController, - itemCount: widget.renderList.elements.length + (widget.topWidget != null ? 1 : 0), - addRepaintBoundaries: true, - shrinkWrap: widget.shrinkWrap, - ); - - final child = (useDragScrolling && ModalRoute.of(context) != null) - ? DraggableScrollbar.semicircle( - scrollStateListener: dragScrolling, - itemPositionsListener: _itemPositionsListener, - controller: _itemScrollController, - backgroundColor: context.isDarkTheme - ? context.colorScheme.primary.darken(amount: .5) - : context.colorScheme.primary, - labelTextBuilder: widget.showLabel ? _labelBuilder : null, - padding: appBarOffset() ? const EdgeInsets.only(top: 60) : const EdgeInsets.only(), - heightOffset: appBarOffset() ? 60 : 0, - labelConstraints: const BoxConstraints(maxHeight: 28), - scrollbarAnimationDuration: const Duration(milliseconds: 300), - scrollbarTimeToFade: const Duration(milliseconds: 1000), - child: listWidget, - ) - : listWidget; - - return widget.onRefresh == null - ? child - : appBarOffset() - ? RefreshIndicator(onRefresh: widget.onRefresh!, edgeOffset: 30, child: child) - : RefreshIndicator(onRefresh: widget.onRefresh!, child: child); - } - - void _scrollToDate() { - final date = scrollToDateNotifierProvider.value; - if (date == null) { - ImmichToast.show( - context: context, - msg: "Scroll To Date failed, date is null.", - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - return; - } - - // Search for the index of the exact date in the list - var index = widget.renderList.elements.indexWhere( - (e) => e.date.year == date.year && e.date.month == date.month && e.date.day == date.day, - ); - - // If the exact date is not found, the timeline is grouped by month, - // thus we search for the month - if (index == -1) { - index = widget.renderList.elements.indexWhere((e) => e.date.year == date.year && e.date.month == date.month); - } - - if (index < widget.renderList.elements.length) { - // Not sure why the index is shifted, but it works. :3 - _scrollToIndex(index + 1); - } else { - ImmichToast.show( - context: context, - msg: "The date (${DateFormat.yMd().format(date)}) could not be found in the timeline.", - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - } - - @override - void didUpdateWidget(ImmichAssetGridView oldWidget) { - super.didUpdateWidget(oldWidget); - if (!widget.selectionActive) { - setState(() { - _selectedAssets.clear(); - }); - } - } - - @override - void initState() { - super.initState(); - currentAssetLink = ref.read(currentAssetProvider.notifier).ref.keepAlive(); - scrollToTopNotifierProvider.addListener(_scrollToTop); - scrollToDateNotifierProvider.addListener(_scrollToDate); - - if (widget.visibleItemsListener != null) { - _itemPositionsListener.itemPositions.addListener(_positionListener); - } - if (widget.preselectedAssets != null) { - _selectedAssets.addAll(widget.preselectedAssets!); - } - - _itemPositionsListener.itemPositions.addListener(_hapticsListener); - } - - @override - void dispose() { - scrollToTopNotifierProvider.removeListener(_scrollToTop); - scrollToDateNotifierProvider.removeListener(_scrollToDate); - if (widget.visibleItemsListener != null) { - _itemPositionsListener.itemPositions.removeListener(_positionListener); - } - _itemPositionsListener.itemPositions.removeListener(_hapticsListener); - currentAssetLink.close(); - super.dispose(); - } - - void _positionListener() { - final values = _itemPositionsListener.itemPositions.value; - widget.visibleItemsListener?.call(values); - } - - void _hapticsListener() { - /// throttle interval for the haptic feedback in microseconds. - /// Currently set to 100ms. - const feedbackInterval = 100000; - - final values = _itemPositionsListener.itemPositions.value; - final start = values.firstOrNull; - - if (start != null) { - final pos = start.index; - final maxLength = widget.renderList.elements.length; - if (pos < 0 || pos >= maxLength) { - return; - } - - final date = widget.renderList.elements[pos].date; - - // only provide the feedback if the prev. date is known. - // Otherwise the app would provide the haptic feedback - // on startup. - if (_prevItemTime == null) { - _prevItemTime = date; - } else if (_prevItemTime?.year != date.year || _prevItemTime?.month != date.month) { - _prevItemTime = date; - - final now = Timeline.now; - if (now > (_hapticFeedbackTS + feedbackInterval)) { - _hapticFeedbackTS = now; - ref.read(hapticFeedbackProvider.notifier).mediumImpact(); - } - } - } - } - - void _scrollToTop() { - // for some reason, this is necessary as well in order - // to correctly reposition the drag thumb scroll bar - _itemScrollController.jumpTo(index: 0); - _itemScrollController.scrollTo(index: 0, duration: const Duration(milliseconds: 200)); - } - - void _setDragStartIndex(AssetIndex index) { - setState(() { - _scrollPhysics = const ClampingScrollPhysics(); - _dragAnchorAssetIndex = index.rowIndex; - _dragAnchorSectionIndex = index.sectionIndex; - _dragging = true; - }); - } - - void _stopDrag() { - WidgetsBinding.instance.addPostFrameCallback((_) { - // Update the physics post frame to prevent sudden change in physics on iOS. - setState(() { - _scrollPhysics = null; - }); - }); - setState(() { - _dragging = false; - _draggedAssets.clear(); - }); - } - - void _dragDragScroll(ScrollDirection direction) { - _scrollOffsetController.animateScroll( - offset: direction == ScrollDirection.forward ? 175 : -175, - duration: const Duration(milliseconds: 125), - ); - } - - void _handleDragAssetEnter(AssetIndex index) { - if (_dragAnchorSectionIndex == null || _dragAnchorAssetIndex == null) { - return; - } - - final dragAnchorSectionIndex = _dragAnchorSectionIndex!; - final dragAnchorAssetIndex = _dragAnchorAssetIndex!; - - late final int startSectionIndex; - late final int startSectionAssetIndex; - late final int endSectionIndex; - late final int endSectionAssetIndex; - - if (index.sectionIndex < dragAnchorSectionIndex) { - startSectionIndex = index.sectionIndex; - startSectionAssetIndex = index.rowIndex; - endSectionIndex = dragAnchorSectionIndex; - endSectionAssetIndex = dragAnchorAssetIndex; - } else if (index.sectionIndex > dragAnchorSectionIndex) { - startSectionIndex = dragAnchorSectionIndex; - startSectionAssetIndex = dragAnchorAssetIndex; - endSectionIndex = index.sectionIndex; - endSectionAssetIndex = index.rowIndex; - } else { - startSectionIndex = dragAnchorSectionIndex; - endSectionIndex = dragAnchorSectionIndex; - - // If same section, assign proper start / end asset Index - if (dragAnchorAssetIndex < index.rowIndex) { - startSectionAssetIndex = dragAnchorAssetIndex; - endSectionAssetIndex = index.rowIndex; - } else { - startSectionAssetIndex = index.rowIndex; - endSectionAssetIndex = dragAnchorAssetIndex; - } - } - - final selectedAssets = {}; - var currentSectionIndex = startSectionIndex; - while (currentSectionIndex < endSectionIndex) { - final section = widget.renderList.elements.elementAtOrNull(currentSectionIndex); - if (section == null) continue; - - final sectionAssets = widget.renderList.loadAssets(section.offset, section.count); - - if (currentSectionIndex == startSectionIndex) { - selectedAssets.addAll(sectionAssets.slice(startSectionAssetIndex, sectionAssets.length)); - } else { - selectedAssets.addAll(sectionAssets); - } - - currentSectionIndex += 1; - } - - final section = widget.renderList.elements.elementAtOrNull(endSectionIndex); - if (section != null) { - final sectionAssets = widget.renderList.loadAssets(section.offset, section.count); - if (startSectionIndex == endSectionIndex) { - selectedAssets.addAll(sectionAssets.slice(startSectionAssetIndex, endSectionAssetIndex + 1)); - } else { - selectedAssets.addAll(sectionAssets.slice(0, endSectionAssetIndex + 1)); - } - } - - _deselectAssets(_draggedAssets.toList()); - _draggedAssets.clear(); - _draggedAssets.addAll(selectedAssets); - _selectAssets(_draggedAssets.toList()); - } - - @override - Widget build(BuildContext context) { - return PopScope( - canPop: !(widget.selectionActive && _selectedAssets.isNotEmpty), - onPopInvokedWithResult: (didPop, _) { - if (didPop) { - return; - } else { - /// `preselectedAssets` is only present when opening the asset grid from the - /// "add to album" button. - /// - /// `_selectedAssets` includes `preselectedAssets` on initialization. - if (_selectedAssets.length > (widget.preselectedAssets?.length ?? 0)) { - /// `_deselectAll` only deselects the selected assets, - /// doesn't affect the preselected ones. - _deselectAll(); - return; - } else { - Navigator.of(context).canPop() ? Navigator.of(context).pop() : null; - } - } - }, - child: Stack( - children: [ - AssetDragRegion( - onStart: _setDragStartIndex, - onAssetEnter: _handleDragAssetEnter, - onEnd: _stopDrag, - onScroll: _dragDragScroll, - onScrollStart: () => - WidgetsBinding.instance.addPostFrameCallback((_) => controlBottomAppBarNotifier.minimize()), - child: _buildAssetGrid(), - ), - if (widget.showMultiSelectIndicator && widget.selectionActive) _buildMultiSelectIndicator(), - ], - ), - ); - } -} - -/// A single row of all placeholder widgets -class _PlaceholderRow extends StatelessWidget { - final int number; - final double width; - final double height; - final double margin; - - const _PlaceholderRow({ - super.key, - required this.number, - required this.width, - required this.height, - required this.margin, - }); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - for (int i = 0; i < number; i++) - ThumbnailPlaceholder( - key: ValueKey(i), - width: width, - height: height, - margin: EdgeInsets.only(bottom: margin, right: i + 1 == number ? 0.0 : margin), - ), - ], - ); - } -} - -/// A section for the render grid -class _Section extends StatelessWidget { - final RenderAssetGridElement section; - final int sectionIndex; - final Set selectedAssets; - final bool scrolling; - final double margin; - final int assetsPerRow; - final RenderList renderList; - final bool selectionActive; - final bool dynamicLayout; - final void Function(List) selectAssets; - final void Function(List) deselectAssets; - final bool Function(List) allAssetsSelected; - final bool showStack; - final int heroOffset; - final bool showStorageIndicator; - final void Function(Asset) onAssetTap; - - const _Section({ - required this.section, - required this.sectionIndex, - required this.scrolling, - required this.margin, - required this.assetsPerRow, - required this.renderList, - required this.selectionActive, - required this.dynamicLayout, - required this.selectAssets, - required this.deselectAssets, - required this.allAssetsSelected, - required this.selectedAssets, - required this.showStack, - required this.heroOffset, - required this.showStorageIndicator, - required this.onAssetTap, - }); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final width = constraints.maxWidth / assetsPerRow - margin * (assetsPerRow - 1) / assetsPerRow; - final rows = (section.count + assetsPerRow - 1) ~/ assetsPerRow; - final List assetsToRender = scrolling ? [] : renderList.loadAssets(section.offset, section.count); - return Column( - key: ValueKey(section.offset), - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (section.type == RenderAssetGridElementType.monthTitle) _MonthTitle(date: section.date), - if (section.type == RenderAssetGridElementType.groupDividerTitle || - section.type == RenderAssetGridElementType.monthTitle) - _Title( - selectionActive: selectionActive, - title: section.title!, - assets: scrolling ? [] : renderList.loadAssets(section.offset, section.totalCount), - allAssetsSelected: allAssetsSelected, - selectAssets: selectAssets, - deselectAssets: deselectAssets, - ), - for (int i = 0; i < rows; i++) - scrolling - ? _PlaceholderRow( - key: ValueKey(i), - number: i + 1 == rows ? section.count - i * assetsPerRow : assetsPerRow, - width: width, - height: width, - margin: margin, - ) - : _AssetRow( - key: ValueKey(i), - rowStartIndex: i * assetsPerRow, - sectionIndex: sectionIndex, - assets: assetsToRender.nestedSlice(i * assetsPerRow, min((i + 1) * assetsPerRow, section.count)), - absoluteOffset: section.offset + i * assetsPerRow, - width: width, - assetsPerRow: assetsPerRow, - margin: margin, - dynamicLayout: dynamicLayout, - renderList: renderList, - selectedAssets: selectedAssets, - isSelectionActive: selectionActive, - showStack: showStack, - heroOffset: heroOffset, - showStorageIndicator: showStorageIndicator, - selectionActive: selectionActive, - onSelect: (asset) => selectAssets([asset]), - onDeselect: (asset) => deselectAssets([asset]), - onAssetTap: onAssetTap, - ), - ], - ); - }, - ); - } -} - -/// The month title row for a section -class _MonthTitle extends StatelessWidget { - final DateTime date; - - const _MonthTitle({required this.date}); - - @override - Widget build(BuildContext context) { - final monthFormat = DateTime.now().year == date.year ? DateFormat.MMMM() : DateFormat.yMMMM(); - final String title = monthFormat.format(date); - return Padding( - key: Key("month-$title"), - padding: const EdgeInsets.only(left: 12.0, top: 24.0), - child: Text( - toBeginningOfSentenceCase(title, context.locale.languageCode), - style: const TextStyle(fontSize: 26, fontWeight: FontWeight.w500), - ), - ); - } -} - -/// A title row -class _Title extends StatelessWidget { - final String title; - final List assets; - final bool selectionActive; - final void Function(List) selectAssets; - final void Function(List) deselectAssets; - final bool Function(List) allAssetsSelected; - - const _Title({ - required this.title, - required this.assets, - required this.selectionActive, - required this.selectAssets, - required this.deselectAssets, - required this.allAssetsSelected, - }); - - @override - Widget build(BuildContext context) { - return GroupDividerTitle( - text: toBeginningOfSentenceCase(title, context.locale.languageCode), - multiselectEnabled: selectionActive, - onSelect: () => selectAssets(assets), - onDeselect: () => deselectAssets(assets), - selected: allAssetsSelected(assets), - ); - } -} - -/// The row of assets -class _AssetRow extends StatelessWidget { - final List assets; - final int rowStartIndex; - final int sectionIndex; - final Set selectedAssets; - final int absoluteOffset; - final double width; - final bool dynamicLayout; - final double margin; - final int assetsPerRow; - final RenderList renderList; - final bool selectionActive; - final bool showStorageIndicator; - final int heroOffset; - final bool showStack; - final void Function(Asset) onAssetTap; - final void Function(Asset)? onSelect; - final void Function(Asset)? onDeselect; - final bool isSelectionActive; - - const _AssetRow({ - super.key, - required this.rowStartIndex, - required this.sectionIndex, - required this.assets, - required this.absoluteOffset, - required this.width, - required this.dynamicLayout, - required this.margin, - required this.assetsPerRow, - required this.renderList, - required this.selectionActive, - required this.showStorageIndicator, - required this.heroOffset, - required this.showStack, - required this.isSelectionActive, - required this.selectedAssets, - required this.onAssetTap, - this.onSelect, - this.onDeselect, - }); - - @override - Widget build(BuildContext context) { - // Default: All assets have the same width - final widthDistribution = List.filled(assets.length, 1.0); - - if (dynamicLayout) { - final aspectRatios = assets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList(); - final meanAspectRatio = aspectRatios.sum / assets.length; - - // 1: mean width - // 0.5: width < mean - threshold - // 1.5: width > mean + threshold - final arConfiguration = aspectRatios.map((e) { - if (e - meanAspectRatio > 0.3) return 1.5; - if (e - meanAspectRatio < -0.3) return 0.5; - return 1.0; - }); - - // Normalize: - final sum = arConfiguration.sum; - widthDistribution.setRange(0, widthDistribution.length, arConfiguration.map((e) => (e * assets.length) / sum)); - } - return Row( - key: key, - children: assets.mapIndexed((int index, Asset asset) { - final bool last = index + 1 == assetsPerRow; - final isSelected = isSelectionActive && selectedAssets.contains(asset); - return Container( - width: width * widthDistribution[index], - height: width, - margin: EdgeInsets.only(bottom: margin, right: last ? 0.0 : margin), - child: GestureDetector( - onTap: () { - if (selectionActive) { - if (isSelected) { - onDeselect?.call(asset); - } else { - onSelect?.call(asset); - } - } else { - final asset = renderList.loadAsset(absoluteOffset + index); - onAssetTap(asset); - context.pushRoute( - GalleryViewerRoute( - renderList: renderList, - initialIndex: absoluteOffset + index, - heroOffset: heroOffset, - showStack: showStack, - ), - ); - } - }, - onLongPress: () { - onSelect?.call(asset); - HapticFeedback.heavyImpact(); - }, - child: AssetIndexWrapper( - rowIndex: rowStartIndex + index, - sectionIndex: sectionIndex, - child: ThumbnailImage( - asset: asset, - multiselectEnabled: selectionActive, - isSelected: isSelectionActive && selectedAssets.contains(asset), - showStorageIndicator: showStorageIndicator, - heroOffset: heroOffset, - showStack: showStack, - ), - ), - ), - ); - }).toList(), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart deleted file mode 100644 index c0d8a6bea2..0000000000 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ /dev/null @@ -1,458 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/asset_selection_state.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/stack.service.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class MultiselectGrid extends HookConsumerWidget { - const MultiselectGrid({ - super.key, - required this.renderListProvider, - this.onRefresh, - this.buildLoadingIndicator, - this.onRemoveFromAlbum, - this.topWidget, - this.stackEnabled = false, - this.dragScrollLabelEnabled = true, - this.archiveEnabled = false, - this.deleteEnabled = true, - this.favoriteEnabled = true, - this.editEnabled = false, - this.unarchive = false, - this.unfavorite = false, - this.downloadEnabled = true, - this.emptyIndicator, - }); - - final ProviderListenable> renderListProvider; - final Future Function()? onRefresh; - final Widget Function()? buildLoadingIndicator; - final Future Function(Iterable)? onRemoveFromAlbum; - final Widget? topWidget; - final bool stackEnabled; - final bool dragScrollLabelEnabled; - final bool archiveEnabled; - final bool unarchive; - final bool deleteEnabled; - final bool downloadEnabled; - final bool favoriteEnabled; - final bool unfavorite; - final bool editEnabled; - final Widget? emptyIndicator; - Widget buildDefaultLoadingIndicator() => const Center(child: CircularProgressIndicator()); - - Widget buildEmptyIndicator() => emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final multiselectEnabled = ref.watch(multiselectProvider.notifier); - final selectionEnabledHook = useState(false); - final selectionAssetState = useState(const AssetSelectionState()); - - final selection = useState({}); - final currentUser = ref.watch(currentUserProvider); - final processing = useProcessingOverlay(); - - useEffect(() { - selectionEnabledHook.addListener(() { - multiselectEnabled.state = selectionEnabledHook.value; - }); - - return () { - // This does not work in tests - if (kReleaseMode) { - selectionEnabledHook.dispose(); - } - }; - }, []); - - void selectionListener(bool multiselect, Set selectedAssets) { - selectionEnabledHook.value = multiselect; - selection.value = selectedAssets; - selectionAssetState.value = AssetSelectionState.fromSelection(selectedAssets); - } - - errorBuilder(String? msg) => msg != null && msg.isNotEmpty - ? () => ImmichToast.show(context: context, msg: msg, gravity: ToastGravity.BOTTOM) - : null; - - Iterable ownedRemoteSelection({String? localErrorMessage, String? ownerErrorMessage}) { - final assets = selection.value; - return assets - .remoteOnly(errorCallback: errorBuilder(localErrorMessage)) - .ownedOnly(currentUser, errorCallback: errorBuilder(ownerErrorMessage)); - } - - Iterable remoteSelection({String? errorMessage}) => - selection.value.remoteOnly(errorCallback: errorBuilder(errorMessage)); - - void onShareAssets(bool shareLocal) { - processing.value = true; - if (shareLocal) { - // Share = Download + Send to OS specific share sheet - handleShareAssets(ref, context, selection.value); - } else { - final ids = remoteSelection(errorMessage: "home_page_share_err_local".tr()).map((e) => e.remoteId!); - context.pushRoute(SharedLinkEditRoute(assetsList: ids.toList())); - } - processing.value = false; - selectionEnabledHook.value = false; - } - - void onFavoriteAssets() async { - processing.value = true; - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_favorite_err_local'.tr(), - ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), - ); - if (remoteAssets.isNotEmpty) { - await handleFavoriteAssets(ref, context, remoteAssets.toList()); - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onArchiveAsset() async { - processing.value = true; - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_archive_err_local'.tr(), - ownerErrorMessage: 'home_page_archive_err_partner'.tr(), - ); - await handleArchiveAssets(ref, context, remoteAssets.toList()); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onDelete([bool force = false]) async { - processing.value = true; - try { - final toDelete = selection.value - .ownedOnly(currentUser, errorCallback: errorBuilder('home_page_delete_err_partner'.tr())) - .toList(); - final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(toDelete, force: force); - - if (isDeleted) { - ImmichToast.show( - context: context, - msg: force - ? 'assets_deleted_permanently'.tr(namedArgs: {'count': "${selection.value.length}"}) - : 'assets_trashed'.tr(namedArgs: {'count': "${selection.value.length}"}), - gravity: ToastGravity.BOTTOM, - ); - selectionEnabledHook.value = false; - } - } finally { - processing.value = false; - } - } - - void onDeleteLocal(bool isMergedAsset) async { - processing.value = true; - try { - final localAssets = selection.value.where((a) => a.isLocal).toList(); - - final toDelete = isMergedAsset ? localAssets.where((e) => e.storage == AssetState.merged) : localAssets; - - if (toDelete.isEmpty) { - return; - } - - final isDeleted = await ref.read(assetProvider.notifier).deleteLocalAssets(toDelete.toList()); - - if (isDeleted) { - final deletedCount = localAssets.where((e) => !isMergedAsset || e.isRemote).length; - - ImmichToast.show( - context: context, - msg: 'assets_removed_permanently_from_device'.tr(namedArgs: {'count': "$deletedCount"}), - gravity: ToastGravity.BOTTOM, - ); - - selectionEnabledHook.value = false; - } - } finally { - processing.value = false; - } - } - - void onDownload() async { - processing.value = true; - try { - final toDownload = selection.value.toList(); - - final results = await ref.read(downloadStateProvider.notifier).downloadAllAsset(toDownload); - - final totalCount = toDownload.length; - final successCount = results.where((e) => e).length; - final failedCount = totalCount - successCount; - - final msg = failedCount > 0 - ? 'assets_downloaded_failed'.t(context: context, args: {'count': successCount, 'error': failedCount}) - : 'assets_downloaded_successfully'.t(context: context, args: {'count': successCount}); - - ImmichToast.show(context: context, msg: msg, gravity: ToastGravity.BOTTOM); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onDeleteRemote([bool shouldDeletePermanently = false]) async { - processing.value = true; - try { - final toDelete = ownedRemoteSelection( - localErrorMessage: 'home_page_delete_remote_err_local'.tr(), - ownerErrorMessage: 'home_page_delete_err_partner'.tr(), - ).toList(); - - final isDeleted = await ref - .read(assetProvider.notifier) - .deleteRemoteAssets(toDelete, shouldDeletePermanently: shouldDeletePermanently); - if (isDeleted) { - ImmichToast.show( - context: context, - msg: shouldDeletePermanently - ? 'assets_deleted_permanently_from_server'.tr(namedArgs: {'count': "${toDelete.length}"}) - : 'assets_trashed_from_server'.tr(namedArgs: {'count': "${toDelete.length}"}), - gravity: ToastGravity.BOTTOM, - ); - } - } finally { - selectionEnabledHook.value = false; - processing.value = false; - } - } - - void onUpload() { - processing.value = true; - selectionEnabledHook.value = false; - try { - ref - .read(manualUploadProvider.notifier) - .uploadAssets(context, selection.value.where((a) => a.storage == AssetState.local)); - } finally { - processing.value = false; - } - } - - void onAddToAlbum(Album album) async { - processing.value = true; - try { - final Iterable assets = remoteSelection(errorMessage: "home_page_add_to_album_err_local".tr()); - if (assets.isEmpty) { - return; - } - final result = await ref.read(albumServiceProvider).addAssets(album, assets); - - if (result != null) { - if (result.alreadyInAlbum.isNotEmpty) { - ImmichToast.show( - context: context, - msg: "home_page_add_to_album_conflicts".tr( - namedArgs: { - "album": album.name, - "added": result.successfullyAdded.toString(), - "failed": result.alreadyInAlbum.length.toString(), - }, - ), - ); - } else { - ImmichToast.show( - context: context, - msg: "home_page_add_to_album_success".tr( - namedArgs: {"album": album.name, "added": result.successfullyAdded.toString()}, - ), - toastType: ToastType.success, - ); - } - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onCreateNewAlbum() async { - processing.value = true; - try { - final Iterable assets = remoteSelection(errorMessage: "home_page_add_to_album_err_local".tr()); - if (assets.isEmpty) { - return; - } - final result = await ref.read(albumServiceProvider).createAlbumWithGeneratedName(assets); - - if (result != null) { - unawaited(ref.watch(albumProvider.notifier).refreshRemoteAlbums()); - selectionEnabledHook.value = false; - - unawaited(context.pushRoute(AlbumViewerRoute(albumId: result.id))); - } - } finally { - processing.value = false; - } - } - - void onStack() async { - try { - processing.value = true; - if (!selectionEnabledHook.value || selection.value.length < 2) { - return; - } - - await ref.read(stackServiceProvider).createStack(selection.value.map((e) => e.remoteId!).toList()); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onEditTime() async { - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_favorite_err_local'.tr(), - ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), - ); - - if (remoteAssets.isNotEmpty) { - unawaited(handleEditDateTime(ref, context, remoteAssets.toList())); - } - } finally { - selectionEnabledHook.value = false; - } - } - - void onEditLocation() async { - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_favorite_err_local'.tr(), - ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), - ); - - if (remoteAssets.isNotEmpty) { - unawaited(handleEditLocation(ref, context, remoteAssets.toList())); - } - } finally { - selectionEnabledHook.value = false; - } - } - - void onToggleLockedVisibility() async { - processing.value = true; - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_locked_error_local'.tr(), - ownerErrorMessage: 'home_page_locked_error_partner'.tr(), - ); - if (remoteAssets.isNotEmpty) { - final isInLockedView = ref.read(inLockedViewProvider); - final visibility = isInLockedView ? AssetVisibilityEnum.timeline : AssetVisibilityEnum.locked; - - await handleSetAssetsVisibility(ref, context, visibility, remoteAssets.toList()); - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - Future Function() wrapLongRunningFun(Future Function() fun, {bool showOverlay = true}) => () async { - if (showOverlay) processing.value = true; - try { - final result = await fun(); - if (result.runtimeType != bool || result == true) { - selectionEnabledHook.value = false; - } - return result; - } finally { - if (showOverlay) processing.value = false; - } - }; - - return SafeArea( - top: true, - bottom: false, - child: Stack( - children: [ - ref - .watch(renderListProvider) - .when( - data: (data) => data.isEmpty && (buildLoadingIndicator != null || topWidget == null) - ? (buildLoadingIndicator ?? buildEmptyIndicator)() - : ImmichAssetGrid( - renderList: data, - listener: selectionListener, - selectionActive: selectionEnabledHook.value, - onRefresh: onRefresh == null ? null : wrapLongRunningFun(onRefresh!, showOverlay: false), - topWidget: topWidget, - showStack: stackEnabled, - showDragScrollLabel: dragScrollLabelEnabled, - ), - error: (error, _) => Center(child: Text(error.toString())), - loading: buildLoadingIndicator ?? buildDefaultLoadingIndicator, - ), - if (selectionEnabledHook.value) - ControlBottomAppBar( - key: const ValueKey("controlBottomAppBar"), - onShare: onShareAssets, - onFavorite: favoriteEnabled ? onFavoriteAssets : null, - onArchive: archiveEnabled ? onArchiveAsset : null, - onDelete: deleteEnabled ? onDelete : null, - onDeleteServer: deleteEnabled ? onDeleteRemote : null, - onDownload: downloadEnabled ? onDownload : null, - - /// local file deletion is allowed irrespective of [deleteEnabled] since it has - /// nothing to do with the state of the asset in the Immich server - onDeleteLocal: onDeleteLocal, - onAddToAlbum: onAddToAlbum, - onCreateNewAlbum: onCreateNewAlbum, - onUpload: onUpload, - enabled: !processing.value, - selectionAssetState: selectionAssetState.value, - selectedAssets: selection.value.toList(), - onStack: stackEnabled ? onStack : null, - onEditTime: editEnabled ? onEditTime : null, - onEditLocation: editEnabled ? onEditLocation : null, - unfavorite: unfavorite, - unarchive: unarchive, - onToggleLocked: onToggleLockedVisibility, - onRemoveFromAlbum: onRemoveFromAlbum != null - ? wrapLongRunningFun(() => onRemoveFromAlbum!(selection.value)) - : null, - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart b/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart deleted file mode 100644 index 3a1fa82a28..0000000000 --- a/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/asset_viewer/render_list_status_provider.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -class MultiselectGridStatusIndicator extends HookConsumerWidget { - const MultiselectGridStatusIndicator({super.key, this.buildLoadingIndicator, this.emptyIndicator}); - - final Widget Function()? buildLoadingIndicator; - final Widget? emptyIndicator; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final renderListStatus = ref.watch(renderListStatusProvider); - return switch (renderListStatus) { - RenderListStatusEnum.loading => - buildLoadingIndicator == null - ? const Center(child: DelayedLoadingIndicator(delay: Duration(milliseconds: 500))) - : buildLoadingIndicator!(), - RenderListStatusEnum.empty => emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()), - RenderListStatusEnum.error => Center(child: const Text("error_loading_assets").tr()), - RenderListStatusEnum.complete => const SizedBox(), - }; - } -} diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart deleted file mode 100644 index 93385b88b3..0000000000 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -class ThumbnailImage extends StatelessWidget { - /// The asset to show the thumbnail image for - final Asset asset; - - /// Whether to show the storage indicator icont over the image or not - final bool showStorageIndicator; - - /// Whether to show the show stack icon over the image or not - final bool showStack; - - /// Whether to show the checkmark indicating that this image is selected - final bool isSelected; - - /// Can override [isSelected] and never show the selection indicator - final bool multiselectEnabled; - - /// If we are allowed to deselect this image - final bool canDeselect; - - /// The offset index to apply to this hero tag for animation - final int heroOffset; - - const ThumbnailImage({ - super.key, - required this.asset, - this.showStorageIndicator = true, - this.showStack = false, - this.isSelected = false, - this.multiselectEnabled = false, - this.heroOffset = 0, - this.canDeselect = true, - }); - - @override - Widget build(BuildContext context) { - final assetContainerColor = context.isDarkTheme - ? context.primaryColor.darken(amount: 0.6) - : context.primaryColor.lighten(amount: 0.8); - - return Stack( - children: [ - AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.decelerate, - decoration: BoxDecoration( - border: multiselectEnabled && isSelected - ? canDeselect - ? Border.all(color: assetContainerColor, width: 8) - : const Border( - top: BorderSide(color: Colors.grey, width: 8), - right: BorderSide(color: Colors.grey, width: 8), - bottom: BorderSide(color: Colors.grey, width: 8), - left: BorderSide(color: Colors.grey, width: 8), - ) - : const Border(), - ), - child: Stack( - children: [ - _ImageIcon( - heroOffset: heroOffset, - asset: asset, - assetContainerColor: assetContainerColor, - multiselectEnabled: multiselectEnabled, - canDeselect: canDeselect, - isSelected: isSelected, - ), - if (showStorageIndicator) _StorageIcon(storage: asset.storage), - if (asset.isFavorite) - const Positioned(left: 8, bottom: 5, child: Icon(Icons.favorite, color: Colors.white, size: 16)), - if (asset.isVideo) _VideoIcon(duration: asset.duration), - if (asset.stackCount > 0) _StackIcon(isVideo: asset.isVideo, stackCount: asset.stackCount), - ], - ), - ), - if (multiselectEnabled) - isSelected - ? const Padding( - padding: EdgeInsets.all(3.0), - child: Align(alignment: Alignment.topLeft, child: _SelectedIcon()), - ) - : const Icon(Icons.circle_outlined, color: Colors.white), - ], - ); - } -} - -class _SelectedIcon extends StatelessWidget { - const _SelectedIcon(); - - @override - Widget build(BuildContext context) { - final assetContainerColor = context.isDarkTheme - ? context.primaryColor.darken(amount: 0.6) - : context.primaryColor.lighten(amount: 0.8); - - return DecoratedBox( - decoration: BoxDecoration(shape: BoxShape.circle, color: assetContainerColor), - child: Icon(Icons.check_circle_rounded, color: context.primaryColor), - ); - } -} - -class _VideoIcon extends StatelessWidget { - final Duration duration; - - const _VideoIcon({required this.duration}); - - @override - Widget build(BuildContext context) { - return Positioned( - top: 5, - right: 8, - child: Row( - children: [ - Text( - duration.format(), - style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), - ), - const SizedBox(width: 3), - const Icon(Icons.play_circle_fill_rounded, color: Colors.white, size: 18), - ], - ), - ); - } -} - -class _StackIcon extends StatelessWidget { - final bool isVideo; - final int stackCount; - - const _StackIcon({required this.isVideo, required this.stackCount}); - - @override - Widget build(BuildContext context) { - return Positioned( - top: isVideo ? 28 : 5, - right: 8, - child: Row( - children: [ - if (stackCount > 1) - Text( - "$stackCount", - style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), - ), - if (stackCount > 1) const SizedBox(width: 3), - const Icon(Icons.burst_mode_rounded, color: Colors.white, size: 18), - ], - ), - ); - } -} - -class _StorageIcon extends StatelessWidget { - final AssetState storage; - - const _StorageIcon({required this.storage}); - - @override - Widget build(BuildContext context) { - return switch (storage) { - AssetState.local => const Positioned( - right: 8, - bottom: 5, - child: Icon( - Icons.cloud_off_outlined, - color: Color.fromRGBO(255, 255, 255, 0.8), - size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], - ), - ), - AssetState.remote => const Positioned( - right: 8, - bottom: 5, - child: Icon( - Icons.cloud_outlined, - color: Color.fromRGBO(255, 255, 255, 0.8), - size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], - ), - ), - AssetState.merged => const Positioned( - right: 8, - bottom: 5, - child: Icon( - Icons.cloud_done_outlined, - color: Color.fromRGBO(255, 255, 255, 0.8), - size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], - ), - ), - }; - } -} - -class _ImageIcon extends StatelessWidget { - final int heroOffset; - final Asset asset; - final Color assetContainerColor; - final bool multiselectEnabled; - final bool canDeselect; - final bool isSelected; - - const _ImageIcon({ - required this.heroOffset, - required this.asset, - required this.assetContainerColor, - required this.multiselectEnabled, - required this.canDeselect, - required this.isSelected, - }); - - @override - Widget build(BuildContext context) { - // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id - final isDto = asset.id == noDbId; - final image = SizedBox.expand( - child: Hero( - tag: isDto ? '${asset.remoteId}-$heroOffset' : asset.id + heroOffset, - child: Stack( - children: [ - SizedBox.expand(child: ImmichThumbnail(asset: asset, height: 250, width: 250)), - const DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color.fromRGBO(0, 0, 0, 0.1), - Colors.transparent, - Colors.transparent, - Color.fromRGBO(0, 0, 0, 0.1), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: [0, 0.3, 0.6, 1], - ), - ), - ), - ], - ), - ), - ); - - if (!multiselectEnabled || !isSelected) { - return image; - } - - return DecoratedBox( - decoration: canDeselect ? BoxDecoration(color: assetContainerColor) : const BoxDecoration(color: Colors.grey), - child: ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(15.0)), child: image), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/upload_dialog.dart b/mobile/lib/widgets/asset_grid/upload_dialog.dart deleted file mode 100644 index 86e2759566..0000000000 --- a/mobile/lib/widgets/asset_grid/upload_dialog.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; - -class UploadDialog extends ConfirmDialog { - final Function onUpload; - - const UploadDialog({super.key, required this.onUpload}) - : super( - title: 'upload_dialog_title', - content: 'upload_dialog_info', - cancel: 'cancel', - ok: 'upload', - onOk: onUpload, - ); -} diff --git a/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart b/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart deleted file mode 100644 index 1a3ef3eac3..0000000000 --- a/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class AdvancedBottomSheet extends HookConsumerWidget { - final Asset assetDetail; - final ScrollController? scrollController; - - const AdvancedBottomSheet({super.key, required this.assetDetail, this.scrollController}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SingleChildScrollView( - controller: scrollController, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 8.0), - child: LayoutBuilder( - builder: (context, constraints) { - // One column - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Align(child: Text("ADVANCED INFO", style: TextStyle(fontSize: 12.0))), - const SizedBox(height: 32.0), - Container( - decoration: BoxDecoration( - color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[200], - borderRadius: const BorderRadius.all(Radius.circular(15.0)), - ), - child: Padding( - padding: const EdgeInsets.only(right: 16.0, left: 16, top: 8, bottom: 16), - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - Align( - alignment: Alignment.centerRight, - child: IconButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: assetDetail.toString())).then((_) { - context.scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - "Copied to clipboard", - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), - ), - ); - }); - }, - icon: Icon(Icons.copy, size: 16.0, color: context.primaryColor), - ), - ), - SelectableText( - assetDetail.toString(), - style: const TextStyle( - fontSize: 12.0, - fontWeight: FontWeight.bold, - fontFamily: "GoogleSansCode", - ), - showCursor: true, - ), - ], - ), - ), - ), - const SizedBox(height: 32.0), - ], - ); - }, - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart deleted file mode 100644 index 22a7deffff..0000000000 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ /dev/null @@ -1,362 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/pages/editing/edit.page.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/stack.service.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; -import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class BottomGalleryBar extends ConsumerWidget { - final ValueNotifier assetIndex; - final bool showStack; - final ValueNotifier stackIndex; - final ValueNotifier totalAssets; - final PageController controller; - final RenderList renderList; - - const BottomGalleryBar({ - super.key, - required this.showStack, - required this.stackIndex, - required this.assetIndex, - required this.controller, - required this.totalAssets, - required this.renderList, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isInLockedView = ref.watch(inLockedViewProvider); - final asset = ref.watch(currentAssetProvider); - if (asset == null) { - return const SizedBox(); - } - final isOwner = asset.ownerId == fastHash(ref.watch(currentUserProvider)?.id ?? ''); - final showControls = ref.watch(showControlsProvider); - final stackId = asset.stackId; - - final stackItems = showStack && stackId != null ? ref.watch(assetStackStateProvider(stackId)) : []; - bool isStackPrimaryAsset = asset.stackPrimaryAssetId == null; - final navStack = AutoRouter.of(context).stackData; - final isTrashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - final isFromTrash = - isTrashEnabled && navStack.length > 2 && navStack.elementAt(navStack.length - 2).name == TrashRoute.name; - final isInAlbum = ref.watch(currentAlbumProvider)?.isRemote ?? false; - - void removeAssetFromStack() { - if (stackIndex.value > 0 && showStack && stackId != null) { - ref.read(assetStackStateProvider(stackId).notifier).removeChild(stackIndex.value - 1); - } - } - - void handleDelete() async { - Future onDelete(bool force) async { - final isDeleted = await ref.read(assetProvider.notifier).deleteAssets({asset}, force: force); - if (isDeleted && isStackPrimaryAsset) { - // Workaround for asset remaining in the gallery - renderList.deleteAsset(asset); - - // `assetIndex == totalAssets.value - 1` handle the case of removing the last asset - // to not throw the error when the next preCache index is called - if (totalAssets.value == 1 || assetIndex.value == totalAssets.value - 1) { - // Handle only one asset - await context.maybePop(); - } - - totalAssets.value -= 1; - } - if (isDeleted) { - ref.read(currentAssetProvider.notifier).set(renderList.loadAsset(assetIndex.value)); - } - return isDeleted; - } - - // Asset is trashed - if (isTrashEnabled && !isFromTrash) { - final isDeleted = await onDelete(false); - if (isDeleted) { - // Can only trash assets stored in server. Local assets are always permanently removed for now - if (context.mounted && asset.isRemote && isStackPrimaryAsset) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_trashed'.tr(), - gravity: ToastGravity.BOTTOM, - ); - } - removeAssetFromStack(); - } - return; - } - - // Asset is permanently removed - unawaited( - showDialog( - context: context, - builder: (BuildContext _) { - return DeleteDialog( - onDelete: () async { - final isDeleted = await onDelete(true); - if (isDeleted) { - removeAssetFromStack(); - } - }, - ); - }, - ), - ); - } - - unStack() async { - if (asset.stackId == null) { - return; - } - - await ref.read(stackServiceProvider).deleteStack(asset.stackId!, stackItems); - } - - void showStackActionItems() { - showModalBottomSheet( - context: context, - enableDrag: false, - builder: (BuildContext ctx) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.filter_none_outlined, size: 18), - onTap: () async { - await unStack(); - ctx.pop(); - await context.maybePop(); - }, - title: const Text("viewer_unstack", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - ), - ], - ), - ), - ); - }, - ); - } - - shareAsset() { - if (asset.isOffline) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_action_share_err_offline'.tr(), - gravity: ToastGravity.BOTTOM, - ); - return; - } - ref.read(downloadStateProvider.notifier).shareAsset(asset, context); - } - - void handleEdit() async { - final image = Image(image: ImmichImage.imageProvider(asset: asset)); - - unawaited( - context.navigator.push( - MaterialPageRoute( - builder: (context) => EditImagePage(asset: asset, image: image, isEdited: false), - ), - ), - ); - } - - handleArchive() { - ref.read(assetProvider.notifier).toggleArchive([asset]); - if (isStackPrimaryAsset) { - context.maybePop(); - return; - } - removeAssetFromStack(); - } - - handleDownload() { - if (asset.isLocal) { - return; - } - if (asset.isOffline) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_action_share_err_offline'.tr(), - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(downloadStateProvider.notifier).downloadAsset(asset); - } - - handleRemoveFromAlbum() async { - final album = ref.read(currentAlbumProvider); - final bool isSuccess = album != null && await ref.read(albumProvider.notifier).removeAsset(album, [asset]); - - if (isSuccess) { - // Workaround for asset remaining in the gallery - renderList.deleteAsset(asset); - - if (totalAssets.value == 1) { - // Handle empty viewer - await context.maybePop(); - } else { - // changing this also for the last asset causes the parent to rebuild with an error - totalAssets.value -= 1; - } - if (assetIndex.value == totalAssets.value && assetIndex.value > 0) { - // handle the case of removing the last asset in the list - assetIndex.value -= 1; - } - } else { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_remove".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - } - - final List> albumActions = [ - { - BottomNavigationBarItem( - icon: Icon(Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded), - label: 'share'.tr(), - tooltip: 'share'.tr(), - ): (_) => - shareAsset(), - }, - if (asset.isImage && !isInLockedView) - { - BottomNavigationBarItem( - icon: const Icon(Icons.tune_outlined), - label: 'edit'.tr(), - tooltip: 'edit'.tr(), - ): (_) => - handleEdit(), - }, - if (isOwner && !isInLockedView) - { - asset.isArchived - ? BottomNavigationBarItem( - icon: const Icon(Icons.unarchive_rounded), - label: 'unarchive'.tr(), - tooltip: 'unarchive'.tr(), - ) - : BottomNavigationBarItem( - icon: const Icon(Icons.archive_outlined), - label: 'archive'.tr(), - tooltip: 'archive'.tr(), - ): (_) => - handleArchive(), - }, - if (isOwner && asset.stackCount > 0 && !isInLockedView) - { - BottomNavigationBarItem( - icon: const Icon(Icons.burst_mode_outlined), - label: 'stack'.tr(), - tooltip: 'stack'.tr(), - ): (_) => - showStackActionItems(), - }, - if (isOwner && !isInAlbum) - { - BottomNavigationBarItem( - icon: const Icon(Icons.delete_outline), - label: 'delete'.tr(), - tooltip: 'delete'.tr(), - ): (_) => - handleDelete(), - }, - if (!isOwner) - { - BottomNavigationBarItem( - icon: const Icon(Icons.download_outlined), - label: 'download'.tr(), - tooltip: 'download'.tr(), - ): (_) => - handleDownload(), - }, - if (isInAlbum) - { - BottomNavigationBarItem( - icon: const Icon(Icons.remove_circle_outline), - label: 'remove_from_album'.tr(), - tooltip: 'remove_from_album'.tr(), - ): (_) => - handleRemoveFromAlbum(), - }, - ]; - return IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: showControls ? 1.0 : 0.0, - child: DecoratedBox( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [Colors.black, Colors.transparent], - ), - ), - position: DecorationPosition.background, - child: Padding( - padding: const EdgeInsets.only(top: 40.0), - child: Column( - children: [ - if (asset.isVideo) VideoControls(videoPlayerName: asset.id.toString()), - BottomNavigationBar( - elevation: 0.0, - backgroundColor: Colors.transparent, - unselectedIconTheme: const IconThemeData(color: Colors.white), - selectedIconTheme: const IconThemeData(color: Colors.white), - unselectedLabelStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500, height: 2.3), - selectedLabelStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500, height: 2.3), - unselectedFontSize: 14, - selectedFontSize: 14, - selectedItemColor: Colors.white, - unselectedItemColor: Colors.white, - showSelectedLabels: true, - showUnselectedLabels: true, - items: albumActions.map((e) => e.keys.first).toList(growable: false), - onTap: (index) { - albumActions[index].values.first.call(index); - }, - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/center_play_button.dart b/mobile/lib/widgets/asset_viewer/center_play_button.dart deleted file mode 100644 index 55d8be8095..0000000000 --- a/mobile/lib/widgets/asset_viewer/center_play_button.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/asset_viewer/animated_play_pause.dart'; - -class CenterPlayButton extends StatelessWidget { - const CenterPlayButton({ - super.key, - required this.backgroundColor, - this.iconColor, - required this.show, - required this.isPlaying, - required this.isFinished, - this.onPressed, - }); - - final Color backgroundColor; - final Color? iconColor; - final bool show; - final bool isPlaying; - final bool isFinished; - final VoidCallback? onPressed; - - @override - Widget build(BuildContext context) { - return Center( - child: UnconstrainedBox( - child: AnimatedOpacity( - opacity: show ? 1.0 : 0.0, - duration: const Duration(milliseconds: 100), - child: DecoratedBox( - decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle), - child: IconButton( - iconSize: 32, - padding: const EdgeInsets.all(12.0), - icon: isFinished - ? Icon(Icons.replay, color: iconColor) - : AnimatedPlayPause(color: iconColor, playing: isPlaying), - onPressed: onPressed, - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart deleted file mode 100644 index 09c0e9d091..0000000000 --- a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/cast/cast_manager_state.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/utils/hooks/timer_hook.dart'; -import 'package:immich_mobile/widgets/asset_viewer/center_play_button.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -class CustomVideoPlayerControls extends HookConsumerWidget { - final String videoId; - final Duration hideTimerDuration; - - const CustomVideoPlayerControls({ - super.key, - required this.videoId, - this.hideTimerDuration = const Duration(seconds: 5), - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetIsVideo = ref.watch(currentAssetProvider.select((asset) => asset != null && asset.isVideo)); - final showControls = ref.watch(showControlsProvider); - final status = ref.watch(videoPlayerProvider(videoId).select((value) => value.status)); - - final cast = ref.watch(castProvider); - - // A timer to hide the controls - final hideTimer = useTimer(hideTimerDuration, () { - if (!context.mounted) { - return; - } - final s = ref.read(videoPlayerProvider(videoId)).status; - - // Do not hide on paused - if (s != VideoPlaybackStatus.paused && s != VideoPlaybackStatus.completed && assetIsVideo) { - ref.read(showControlsProvider.notifier).show = false; - } - }); - final showBuffering = status == VideoPlaybackStatus.buffering && !cast.isCasting; - - /// Shows the controls and starts the timer to hide them - void showControlsAndStartHideTimer() { - hideTimer.reset(); - ref.read(showControlsProvider.notifier).show = true; - } - - // When playback starts, reset the hide timer - ref.listen(videoPlayerProvider(videoId).select((v) => v.status), (previous, next) { - if (next == VideoPlaybackStatus.playing) { - hideTimer.reset(); - } - }); - - /// Toggles between playing and pausing depending on the state of the video - void togglePlay() { - showControlsAndStartHideTimer(); - - if (cast.isCasting) { - if (cast.castState == CastState.playing) { - ref.read(castProvider.notifier).pause(); - } else if (cast.castState == CastState.paused) { - ref.read(castProvider.notifier).play(); - } else if (cast.castState == CastState.idle) { - // resend the play command since its finished - final asset = ref.read(currentAssetProvider); - if (asset == null) { - return; - } - ref.read(castProvider.notifier).loadMediaOld(asset, true); - } - return; - } - - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - if (status == VideoPlaybackStatus.playing) { - notifier.pause(); - } else if (status == VideoPlaybackStatus.completed) { - notifier.restart(); - } else { - notifier.play(); - } - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: showControlsAndStartHideTimer, - child: AbsorbPointer( - absorbing: !showControls, - child: Stack( - children: [ - if (showBuffering) - const Center(child: DelayedLoadingIndicator(fadeInDuration: Duration(milliseconds: 400))) - else - GestureDetector( - onTap: () => ref.read(showControlsProvider.notifier).show = false, - child: CenterPlayButton( - backgroundColor: Colors.black54, - iconColor: Colors.white, - isFinished: status == VideoPlaybackStatus.completed, - isPlaying: - status == VideoPlaybackStatus.playing || (cast.isCasting && cast.castState == CastState.playing), - show: assetIsVideo && showControls, - onPressed: togglePlay, - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/description_input.dart b/mobile/lib/widgets/asset_viewer/description_input.dart deleted file mode 100644 index b0cefd63fa..0000000000 --- a/mobile/lib/widgets/asset_viewer/description_input.dart +++ /dev/null @@ -1,106 +0,0 @@ -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/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:logging/logging.dart'; - -class DescriptionInput extends HookConsumerWidget { - DescriptionInput({super.key, required this.asset, this.exifInfo}); - - final Asset asset; - final ExifInfo? exifInfo; - final Logger _log = Logger('DescriptionInput'); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final controller = useTextEditingController(); - final focusNode = useFocusNode(); - final isFocus = useState(false); - final isTextEmpty = useState(controller.text.isEmpty); - final assetService = ref.watch(assetServiceProvider); - final owner = ref.watch(currentUserProvider); - final hasError = useState(false); - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final hasDescription = useState(false); - final isOwner = fastHash(owner?.id ?? '') == asset.ownerId; - - useEffect(() { - assetService.getDescription(asset).then((value) { - controller.text = value; - hasDescription.value = value.isNotEmpty; - }); - return null; - }, [assetWithExif.value]); - - if (!isOwner && !hasDescription.value) { - return const SizedBox.shrink(); - } - - submitDescription(String description) async { - hasError.value = false; - try { - await assetService.setDescription(asset, description); - controller.text = description; - } catch (error, stack) { - hasError.value = true; - _log.severe("Error updating description", error, stack); - ImmichToast.show(context: context, msg: "description_input_submit_error".tr(), toastType: ToastType.error); - } - } - - Widget? suffixIcon; - if (hasError.value) { - suffixIcon = const Icon(Icons.warning_outlined); - } else if (!isTextEmpty.value && isFocus.value) { - suffixIcon = IconButton( - onPressed: () { - controller.clear(); - isTextEmpty.value = true; - }, - icon: Icon(Icons.cancel_rounded, color: context.colorScheme.onSurfaceSecondary), - splashRadius: 10, - ); - } - - return TextField( - enabled: isOwner, - focusNode: focusNode, - onTap: () => isFocus.value = true, - onChanged: (value) { - isTextEmpty.value = false; - }, - onTapOutside: (a) async { - isFocus.value = false; - focusNode.unfocus(); - - if (exifInfo?.description != controller.text) { - await submitDescription(controller.text); - } - }, - autofocus: false, - maxLines: null, - keyboardType: TextInputType.multiline, - controller: controller, - style: context.textTheme.labelLarge, - decoration: InputDecoration( - hintText: 'description_input_hint_text'.tr(), - border: InputBorder.none, - suffixIcon: suffixIcon, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart deleted file mode 100644 index df8f6593df..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; - -class AssetDateTime extends ConsumerWidget { - final Asset asset; - - const AssetDateTime({super.key, required this.asset}); - - String getDateTimeString(Asset a) { - final (deltaTime, timeZone) = a.getTZAdjustedTimeAndOffset(); - final date = DateFormat.yMMMEd().format(deltaTime); - final time = DateFormat.jm().format(deltaTime); - return '$date â€ĸ $time GMT${timeZone.formatAsOffset()}'; - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final watchedAsset = ref.watch(assetDetailProvider(asset)); - String formattedDateTime = getDateTimeString(asset); - - void editDateTime() async { - await handleEditDateTime(ref, context, [asset]); - - if (watchedAsset.value != null) { - formattedDateTime = getDateTimeString(watchedAsset.value!); - } - } - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(formattedDateTime, style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600)), - if (asset.isRemote) IconButton(onPressed: editDateTime, icon: const Icon(Icons.edit_outlined), iconSize: 20), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart deleted file mode 100644 index f0f9a2efcb..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/camera_info.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/file_info.dart'; - -class AssetDetails extends ConsumerWidget { - final Asset asset; - final ExifInfo? exifInfo; - - const AssetDetails({super.key, required this.asset, this.exifInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; - - return Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "exif_bottom_sheet_details", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - FileInfo(asset: asset), - if (exifInfo?.make != null) CameraInfo(exifInfo: exifInfo!), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart deleted file mode 100644 index 6edf226e8b..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart'; - -class AssetLocation extends HookConsumerWidget { - final Asset asset; - - const AssetLocation({super.key, required this.asset}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; - final hasCoordinates = exifInfo?.hasCoordinates ?? false; - - void editLocation() { - handleEditLocation(ref, context, [assetWithExif.value ?? asset]); - } - - // Guard no lat/lng - if (!hasCoordinates) { - return asset.isRemote - ? ListTile( - minLeadingWidth: 0, - contentPadding: const EdgeInsets.all(0), - leading: const Icon(Icons.location_on), - title: Text( - "add_a_location", - style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor), - ).tr(), - onTap: editLocation, - ) - : const SizedBox.shrink(); - } - - Widget getLocationName() { - if (exifInfo == null) { - return const SizedBox.shrink(); - } - - final cityName = exifInfo.city; - final stateName = exifInfo.state; - - bool hasLocationName = (cityName != null && stateName != null); - - return hasLocationName - ? Text("$cityName, $stateName", style: context.textTheme.labelLarge) - : const SizedBox.shrink(); - } - - return Padding( - padding: EdgeInsets.only(top: asset.isRemote ? 0 : 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "exif_bottom_sheet_location", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - if (asset.isRemote) - IconButton(onPressed: editLocation, icon: const Icon(Icons.edit_outlined), iconSize: 20), - ], - ), - asset.isRemote ? const SizedBox.shrink() : const SizedBox(height: 16), - ExifMap(exifInfo: exifInfo!, markerId: asset.remoteId, markerAssetThumbhash: asset.thumbhash), - const SizedBox(height: 16), - getLocationName(), - Text( - "${exifInfo.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}", - style: context.textTheme.labelMedium?.copyWith(color: context.textTheme.labelMedium?.color?.withAlpha(150)), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart deleted file mode 100644 index 5ae29d32c7..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class CameraInfo extends StatelessWidget { - final ExifInfo exifInfo; - - const CameraInfo({super.key, required this.exifInfo}); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - return ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon(Icons.camera, color: textColor.withAlpha(200)), - title: Text("${exifInfo.make} ${exifInfo.model}", style: context.textTheme.labelLarge), - subtitle: exifInfo.f != null || exifInfo.exposureSeconds != null || exifInfo.mm != null || exifInfo.iso != null - ? Text( - "ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ", - style: context.textTheme.bodySmall, - ) - : null, - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart b/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart deleted file mode 100644 index 97c9477c97..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/widgets/asset_viewer/description_input.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_date_time.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_details.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_location.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/people_info.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class DetailPanel extends HookConsumerWidget { - final Asset asset; - final ScrollController? scrollController; - - const DetailPanel({super.key, required this.asset, this.scrollController}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ListView( - controller: scrollController, - shrinkWrap: true, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - AssetDateTime(asset: asset), - asset.isRemote ? DescriptionInput(asset: asset) : const SizedBox.shrink(), - PeopleInfo(asset: asset), - AssetLocation(asset: asset), - AssetDetails(asset: asset), - ], - ), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart deleted file mode 100644 index 78d9ac1776..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/utils/bytes_units.dart'; - -class FileInfo extends StatelessWidget { - final Asset asset; - - const FileInfo({super.key, required this.asset}); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - - final height = asset.orientatedHeight ?? asset.height; - final width = asset.orientatedWidth ?? asset.width; - String resolution = height != null && width != null ? "$width x $height " : ""; - String fileSize = asset.exifInfo?.fileSize != null ? formatBytes(asset.exifInfo!.fileSize!) : ""; - String text = resolution + fileSize; - final imgSizeString = text.isNotEmpty ? text : null; - - String? title; - String? subtitle; - - if (imgSizeString == null && asset.fileName.isNotEmpty) { - // There is only filename - title = asset.fileName; - } else if (imgSizeString != null && asset.fileName.isNotEmpty) { - // There is both filename and size information - title = asset.fileName; - subtitle = imgSizeString; - } else if (imgSizeString != null && asset.fileName.isEmpty) { - title = imgSizeString; - } else { - return const SizedBox.shrink(); - } - - return ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon(Icons.image, color: textColor.withAlpha(200)), - titleAlignment: ListTileTitleAlignment.center, - title: Text(title, style: context.textTheme.labelLarge), - subtitle: subtitle == null ? null : Text(subtitle), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart deleted file mode 100644 index b96cbc777d..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart +++ /dev/null @@ -1,91 +0,0 @@ -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/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_people.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/people.utils.dart'; -import 'package:immich_mobile/widgets/search/curated_people_row.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; - -class PeopleInfo extends ConsumerWidget { - final Asset asset; - final EdgeInsets? padding; - - const PeopleInfo({super.key, required this.asset, this.padding}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final peopleProvider = ref.watch(assetPeopleNotifierProvider(asset).notifier); - final people = ref.watch(assetPeopleNotifierProvider(asset)).value?.where((p) => !p.isHidden); - - showPersonNameEditModel(String personId, String personName) { - return showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: personName); - }, - ).then((_) { - // ensure the people list is up-to-date. - peopleProvider.refresh(); - }); - } - - final curatedPeople = - people - ?.map( - (p) => SearchCuratedContent( - id: p.id, - label: p.name, - subtitle: p.birthDate != null && p.birthDate!.isBefore(asset.fileCreatedAt) - ? formatAge(p.birthDate!, asset.fileCreatedAt) - : null, - ), - ) - .toList() ?? - []; - - return AnimatedCrossFade( - crossFadeState: (people?.isEmpty ?? true) ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 200), - firstChild: Container(), - secondChild: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - children: [ - Padding( - padding: padding ?? EdgeInsets.zero, - child: Align( - alignment: Alignment.topLeft, - child: Text( - "exif_bottom_sheet_people", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: CuratedPeopleRow( - padding: padding, - content: curatedPeople, - onTap: (content, index) { - context - .pushRoute(PersonResultRoute(personId: content.id, personName: content.label)) - .then((_) => peopleProvider.refresh()); - }, - onNameTap: (person, index) => {showPersonNameEditModel(person.id, person.label)}, - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart deleted file mode 100644 index dcb0334801..0000000000 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/providers/trash.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; -import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart'; -import 'package:immich_mobile/widgets/asset_viewer/top_control_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class GalleryAppBar extends ConsumerWidget { - final void Function() showInfo; - - const GalleryAppBar({super.key, required this.showInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = ref.watch(currentAssetProvider); - if (asset == null) { - return const SizedBox(); - } - final album = ref.watch(currentAlbumProvider); - final isOwner = asset.ownerId == fastHash(ref.watch(currentUserProvider)?.id ?? ''); - final showControls = ref.watch(showControlsProvider); - - final isPartner = ref.watch(partnerSharedWithProvider).map((e) => fastHash(e.id)).contains(asset.ownerId); - - toggleFavorite(Asset asset) => ref.read(assetProvider.notifier).toggleFavorite([asset]); - - handleActivities() { - if (album != null && album.shared && album.remoteId != null) { - context.pushRoute(const ActivitiesRoute()); - } - } - - handleRestore(Asset asset) async { - final result = await ref.read(trashProvider.notifier).restoreAssets([asset]); - - if (result && context.mounted) { - ImmichToast.show(context: context, msg: 'asset_restored_successfully'.tr(), gravity: ToastGravity.BOTTOM); - } - } - - handleUpload(Asset asset) { - showDialog( - context: context, - builder: (BuildContext _) { - return UploadDialog( - onUpload: () { - ref.read(manualUploadProvider.notifier).uploadAssets(context, [asset]); - }, - ); - }, - ); - } - - addToAlbum(Asset addToAlbumAsset) { - showModalBottomSheet( - elevation: 0, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), - context: context, - builder: (BuildContext _) { - return AddToAlbumBottomSheet(assets: [addToAlbumAsset]); - }, - ); - } - - handleDownloadAsset() { - ref.read(downloadStateProvider.notifier).downloadAsset(asset); - } - - handleLocateAsset() async { - // Go back to the gallery - await context.maybePop(); - await context.navigateTo(const TabControllerRoute(children: [PhotosRoute()])); - ref.read(tabProvider.notifier).update((state) => state = TabEnum.home); - // Scroll to the asset's date - scrollToDateNotifierProvider.scrollToDate(asset.fileCreatedAt); - } - - return IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: showControls ? 1.0 : 0.0, - child: Container( - color: Colors.black.withValues(alpha: 0.4), - child: TopControlAppBar( - isOwner: isOwner, - isPartner: isPartner, - asset: asset, - onMoreInfoPressed: showInfo, - onLocatePressed: handleLocateAsset, - onFavorite: toggleFavorite, - onRestorePressed: () => handleRestore(asset), - onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null, - onDownloadPressed: asset.isLocal ? null : handleDownloadAsset, - onAddToAlbumPressed: () => addToAlbum(asset), - onActivitiesPressed: handleActivities, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart b/mobile/lib/widgets/asset_viewer/motion_photo_button.dart deleted file mode 100644 index f5479ab86e..0000000000 --- a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; - -class MotionPhotoButton extends ConsumerWidget { - const MotionPhotoButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isPlaying = ref.watch(isPlayingMotionVideoProvider); - - return IconButton( - onPressed: () { - ref.read(isPlayingMotionVideoProvider.notifier).toggle(); - }, - icon: isPlaying - ? const Icon(Icons.motion_photos_pause_outlined, color: grey200) - : const Icon(Icons.play_circle_outline_rounded, color: grey200), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart deleted file mode 100644 index 35f3840797..0000000000 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; -import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; - -class TopControlAppBar extends HookConsumerWidget { - const TopControlAppBar({ - super.key, - required this.asset, - required this.onMoreInfoPressed, - required this.onDownloadPressed, - required this.onLocatePressed, - required this.onAddToAlbumPressed, - required this.onRestorePressed, - required this.onFavorite, - required this.onUploadPressed, - required this.isOwner, - required this.onActivitiesPressed, - required this.isPartner, - }); - - final Asset asset; - final Function onMoreInfoPressed; - final VoidCallback? onUploadPressed; - final VoidCallback? onDownloadPressed; - final VoidCallback onLocatePressed; - final VoidCallback onAddToAlbumPressed; - final VoidCallback onRestorePressed; - final VoidCallback onActivitiesPressed; - final Function(Asset) onFavorite; - final bool isOwner; - final bool isPartner; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isInLockedView = ref.watch(inLockedViewProvider); - const double iconSize = 22.0; - final a = ref.watch(assetWatcher(asset)).value ?? asset; - final album = ref.watch(currentAlbumProvider); - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - final websocketConnected = ref.watch(websocketProvider.select((c) => c.isConnected)); - - final comments = album != null && album.remoteId != null && asset.remoteId != null - ? ref.watch(activityStatisticsProvider(album.remoteId!, asset.remoteId)) - : 0; - - Widget buildFavoriteButton(a) { - return IconButton( - onPressed: () => onFavorite(a), - icon: Icon(a.isFavorite ? Icons.favorite : Icons.favorite_border, color: Colors.grey[200]), - ); - } - - Widget buildLocateButton() { - return IconButton( - onPressed: () { - onLocatePressed(); - }, - icon: Icon(Icons.image_search, color: Colors.grey[200]), - ); - } - - Widget buildMoreInfoButton() { - return IconButton( - onPressed: () { - onMoreInfoPressed(); - }, - icon: Icon(Icons.info_outline_rounded, color: Colors.grey[200]), - ); - } - - Widget buildDownloadButton() { - return IconButton( - onPressed: onDownloadPressed, - icon: Icon(Icons.cloud_download_outlined, color: Colors.grey[200]), - ); - } - - Widget buildAddToAlbumButton() { - return IconButton( - onPressed: () { - onAddToAlbumPressed(); - }, - icon: Icon(Icons.add, color: Colors.grey[200]), - ); - } - - Widget buildRestoreButton() { - return IconButton( - onPressed: () { - onRestorePressed(); - }, - icon: Icon(Icons.history_rounded, color: Colors.grey[200]), - ); - } - - Widget buildActivitiesButton() { - return IconButton( - onPressed: () { - onActivitiesPressed(); - }, - icon: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(Icons.mode_comment_outlined, color: Colors.grey[200]), - if (comments != 0) - Padding( - padding: const EdgeInsets.only(left: 5), - child: Text( - comments.toString(), - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey[200]), - ), - ), - ], - ), - ); - } - - Widget buildUploadButton() { - return IconButton( - onPressed: onUploadPressed, - icon: Icon(Icons.backup_outlined, color: Colors.grey[200]), - ); - } - - Widget buildBackButton() { - return IconButton( - onPressed: () { - context.maybePop(); - }, - icon: Icon(Icons.arrow_back_ios_new_rounded, size: 20.0, color: Colors.grey[200]), - ); - } - - Widget buildCastButton() { - return IconButton( - onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); - }, - icon: Icon( - isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, - size: 20.0, - color: isCasting ? context.primaryColor : Colors.grey[200], - ), - ); - } - - bool isInHomePage = ref.read(tabProvider.notifier).state == TabEnum.home; - bool? isInTrash = ref.read(currentAssetProvider)?.isTrashed; - - return AppBar( - foregroundColor: Colors.grey[100], - backgroundColor: Colors.transparent, - leading: buildBackButton(), - actionsIconTheme: const IconThemeData(size: iconSize), - shape: const Border(), - actions: [ - if (asset.isRemote && isOwner) buildFavoriteButton(a), - if (isOwner && !isInHomePage && !(isInTrash ?? false) && !isInLockedView) buildLocateButton(), - if (asset.livePhotoVideoId != null) const MotionPhotoButton(), - if (asset.isLocal && !asset.isRemote) buildUploadButton(), - if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(), - if (asset.isRemote && (isOwner || isPartner) && !asset.isTrashed && !isInLockedView) buildAddToAlbumButton(), - if (isCasting || (asset.isRemote && websocketConnected)) buildCastButton(), - if (asset.isTrashed) buildRestoreButton(), - if (album != null && album.shared && !isInLockedView) buildActivitiesButton(), - buildMoreInfoButton(), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/video_controls.dart b/mobile/lib/widgets/asset_viewer/video_controls.dart index 85707c82ea..89b0f0ec30 100644 --- a/mobile/lib/widgets/asset_viewer/video_controls.dart +++ b/mobile/lib/widgets/asset_viewer/video_controls.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:async/async.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/colors.dart'; @@ -7,26 +8,63 @@ import 'package:immich_mobile/models/cast/cast_manager_state.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/utils/hooks/timer_hook.dart'; import 'package:immich_mobile/extensions/duration_extensions.dart'; import 'package:immich_mobile/widgets/asset_viewer/animated_play_pause.dart'; -class VideoControls extends HookConsumerWidget { +class VideoControls extends ConsumerStatefulWidget { final String videoPlayerName; static const List _controlShadows = [Shadow(color: Colors.black87, blurRadius: 6, offset: Offset(0, 1))]; const VideoControls({super.key, required this.videoPlayerName}); - void _toggle(WidgetRef ref, bool isCasting) { - if (isCasting) { - ref.read(castProvider.notifier).toggle(); - } else { - ref.read(videoPlayerProvider(videoPlayerName).notifier).toggle(); + @override + ConsumerState createState() => _VideoControlsState(); +} + +class _VideoControlsState extends ConsumerState { + late final RestartableTimer _hideTimer; + + AutoDisposeStateNotifierProvider get _provider => + videoPlayerProvider(widget.videoPlayerName); + + @override + void initState() { + super.initState(); + _hideTimer = RestartableTimer(const Duration(seconds: 5), _onHideTimer); + } + + @override + void didUpdateWidget(covariant VideoControls oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.videoPlayerName != widget.videoPlayerName) { + _hideTimer.reset(); } } - void _onSeek(WidgetRef ref, bool isCasting, double value) { + @override + void dispose() { + _hideTimer.cancel(); + super.dispose(); + } + + void _onHideTimer() { + if (!mounted) return; + if (ref.read(_provider).status == VideoPlaybackStatus.playing) { + ref.read(assetViewerProvider.notifier).setControls(false); + } + } + + void _toggle(bool isCasting) { + if (isCasting) { + ref.read(castProvider.notifier).toggle(); + return; + } + + ref.read(_provider.notifier).toggle(); + } + + void _onSeek(bool isCasting, double value) { final seekTo = Duration(microseconds: value.toInt()); if (isCasting) { @@ -34,41 +72,36 @@ class VideoControls extends HookConsumerWidget { return; } - ref.read(videoPlayerProvider(videoPlayerName).notifier).seekTo(seekTo); + ref.read(_provider.notifier).seekTo(seekTo); } @override - Widget build(BuildContext context, WidgetRef ref) { - final provider = videoPlayerProvider(videoPlayerName); + Widget build(BuildContext context) { final cast = ref.watch(castProvider); final isCasting = cast.isCasting; final (position, duration) = isCasting ? ref.watch(castProvider.select((c) => (c.currentTime, c.duration))) - : ref.watch(provider.select((v) => (v.position, v.duration))); + : ref.watch(_provider.select((v) => (v.position, v.duration))); - final videoStatus = ref.watch(provider.select((v) => v.status)); + final videoStatus = ref.watch(_provider.select((v) => v.status)); final isPlaying = isCasting ? cast.castState == CastState.playing : videoStatus == VideoPlaybackStatus.playing || videoStatus == VideoPlaybackStatus.buffering; final isFinished = !isCasting && videoStatus == VideoPlaybackStatus.completed; - final hideTimer = useTimer(const Duration(seconds: 5), () { - if (!context.mounted) return; - if (ref.read(provider).status == VideoPlaybackStatus.playing) { - ref.read(assetViewerProvider.notifier).setControls(false); - } + ref.listen(assetViewerProvider.select((v) => v.showingControls), (prev, showing) { + if (showing && prev != showing) _hideTimer.reset(); }); + ref.listen(_provider.select((v) => v.status), (_, __) => _hideTimer.reset()); - ref.listen(provider.select((v) => v.status), (_, __) => hideTimer.reset()); - - final notifier = ref.read(provider.notifier); + final notifier = ref.read(_provider.notifier); final isLoaded = duration != Duration.zero; return Padding( - padding: const EdgeInsets.all(24), + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 12), child: Column( - spacing: 16, + spacing: 4, children: [ Row( children: [ @@ -77,9 +110,13 @@ class VideoControls extends HookConsumerWidget { padding: const EdgeInsets.all(12), constraints: const BoxConstraints(), icon: isFinished - ? const Icon(Icons.replay, color: Colors.white, size: 32, shadows: _controlShadows) - : AnimatedPlayPause(color: Colors.white, size: 32, playing: isPlaying, shadows: _controlShadows), - onPressed: () => _toggle(ref, isCasting), + ? const Icon(Icons.replay, color: Colors.white, shadows: VideoControls._controlShadows) + : AnimatedPlayPause( + color: Colors.white, + playing: isPlaying, + shadows: VideoControls._controlShadows, + ), + onPressed: () => _toggle(isCasting), ), const Spacer(), Text( @@ -88,10 +125,10 @@ class VideoControls extends HookConsumerWidget { color: Colors.white, fontWeight: FontWeight.w500, fontFeatures: [FontFeature.tabularFigures()], - shadows: _controlShadows, + shadows: VideoControls._controlShadows, ), ), - const SizedBox(width: 16), + const SizedBox(width: 12), ], ), Slider( @@ -104,7 +141,7 @@ class VideoControls extends HookConsumerWidget { padding: EdgeInsets.zero, onChangeStart: (_) => notifier.hold(), onChangeEnd: (_) => notifier.release(), - onChanged: isLoaded ? (value) => _onSeek(ref, isCasting, value) : null, + onChanged: isLoaded ? (value) => _onSeek(isCasting, value) : null, ), ], ), diff --git a/mobile/lib/widgets/backup/album_info_card.dart b/mobile/lib/widgets/backup/album_info_card.dart deleted file mode 100644 index d635e136bc..0000000000 --- a/mobile/lib/widgets/backup/album_info_card.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/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 AlbumInfoCard extends HookConsumerWidget { - final AvailableAlbum album; - - const AlbumInfoCard({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(album); - final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); - final syncAlbum = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); - - final isDarkTheme = context.isDarkTheme; - - ColorFilter selectedFilter = ColorFilter.mode(context.primaryColor.withAlpha(100), BlendMode.darken); - ColorFilter excludedFilter = ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken); - ColorFilter unselectedFilter = const ColorFilter.mode(Colors.black, BlendMode.color); - - buildSelectedTextBox() { - if (isSelected) { - return Chip( - visualDensity: VisualDensity.compact, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - label: Text( - "album_info_card_backup_album_included", - style: TextStyle( - fontSize: 10, - color: isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - ).tr(), - backgroundColor: context.primaryColor, - ); - } else if (isExcluded) { - return Chip( - visualDensity: VisualDensity.compact, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - label: Text( - "album_info_card_backup_album_excluded", - style: TextStyle( - fontSize: 10, - color: isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - ).tr(), - backgroundColor: Colors.red[300], - ); - } - - return const SizedBox(); - } - - buildImageFilter() { - if (isSelected) { - return selectedFilter; - } else if (isExcluded) { - return excludedFilter; - } else { - return unselectedFilter; - } - } - - return GestureDetector( - onTap: () { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - - if (isSelected) { - ref.read(backupProvider.notifier).removeAlbumForBackup(album); - } else { - ref.read(backupProvider.notifier).addAlbumForBackup(album); - if (syncAlbum) { - ref.read(albumProvider.notifier).createSyncAlbum(album.name); - } - } - }, - onDoubleTap: () { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - - if (isExcluded) { - // Remove from exclude album list - ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album); - } else { - // Add to exclude album list - - if (album.id == 'isAll' || album.name == 'Recents') { - ImmichToast.show( - context: context, - msg: 'Cannot exclude album contains all assets', - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album); - } - }, - child: Card( - clipBehavior: Clip.hardEdge, - margin: const EdgeInsets.all(1), - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all( - Radius.circular(12), // if you need this - ), - side: BorderSide( - color: isDarkTheme ? const Color.fromARGB(255, 37, 35, 35) : const Color(0xFFC9C9C9), - width: 1, - ), - ), - elevation: 0, - borderOnForeground: false, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Stack( - clipBehavior: Clip.hardEdge, - children: [ - ColorFiltered( - colorFilter: buildImageFilter(), - child: const Image( - width: double.infinity, - height: double.infinity, - image: AssetImage('assets/immich-logo.png'), - fit: BoxFit.cover, - ), - ), - Positioned(bottom: 10, right: 25, child: buildSelectedTextBox()), - ], - ), - ), - Padding( - padding: const EdgeInsets.only(left: 25), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - album.name, - style: TextStyle(fontSize: 14, color: context.primaryColor, fontWeight: FontWeight.bold), - ), - Padding( - padding: const EdgeInsets.only(top: 2.0), - child: Text( - album.assetCount.toString() + (album.isAll ? " (${'all'.tr()})" : ""), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - ), - ], - ), - ), - IconButton( - onPressed: () { - context.pushRoute(AlbumPreviewRoute(album: album.album)); - }, - icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24), - splashRadius: 25, - ), - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart deleted file mode 100644 index 9796f45e8b..0000000000 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumInfoListTile extends HookConsumerWidget { - final AvailableAlbum album; - - const AlbumInfoListTile({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(album); - final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); - final syncAlbum = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); - - buildTileColor() { - if (isSelected) { - return context.isDarkTheme ? context.primaryColor.withAlpha(100) : context.primaryColor.withAlpha(25); - } else if (isExcluded) { - return context.isDarkTheme ? Colors.red[300]?.withAlpha(150) : Colors.red[100]?.withAlpha(150); - } else { - return Colors.transparent; - } - } - - buildIcon() { - if (isSelected) { - return Icon(Icons.check_circle_rounded, color: context.colorScheme.primary); - } - - if (isExcluded) { - return Icon(Icons.remove_circle_rounded, color: context.colorScheme.error); - } - - return Icon(Icons.circle, color: context.colorScheme.surfaceContainerHighest); - } - - return GestureDetector( - onDoubleTap: () { - ref.watch(hapticFeedbackProvider.notifier).selectionClick(); - - if (isExcluded) { - // Remove from exclude album list - ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album); - } else { - // Add to exclude album list - - if (album.id == 'isAll' || album.name == 'Recents') { - ImmichToast.show( - context: context, - msg: 'Cannot exclude album contains all assets', - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album); - } - }, - child: ListTile( - tileColor: buildTileColor(), - contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - onTap: () { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - if (isSelected) { - ref.read(backupProvider.notifier).removeAlbumForBackup(album); - } else { - ref.read(backupProvider.notifier).addAlbumForBackup(album); - if (syncAlbum) { - ref.read(albumProvider.notifier).createSyncAlbum(album.name); - } - } - }, - leading: buildIcon(), - title: Text(album.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - subtitle: Text(album.assetCount.toString()), - trailing: IconButton( - onPressed: () { - context.pushRoute(AlbumPreviewRoute(album: album.album)); - }, - icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24), - splashRadius: 25, - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/asset_info_table.dart b/mobile/lib/widgets/backup/asset_info_table.dart deleted file mode 100644 index 2cccded2bb..0000000000 --- a/mobile/lib/widgets/backup/asset_info_table.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class BackupAssetInfoTable extends ConsumerWidget { - const BackupAssetInfoTable({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final isUploadInProgress = ref.watch( - backupProvider.select( - (value) => - value.backupProgress == BackUpProgressEnum.inProgress || - value.backupProgress == BackUpProgressEnum.inBackground || - value.backupProgress == BackUpProgressEnum.manualInProgress, - ), - ); - - final asset = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset)) - : ref.watch(backupProvider.select((value) => value.currentUploadAsset)); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Table( - border: TableBorder.all(color: context.colorScheme.outlineVariant, width: 1), - children: [ - TableRow( - children: [ - TableCell( - verticalAlignment: TableCellVerticalAlignment.middle, - child: Padding( - padding: const EdgeInsets.all(6.0), - child: - Text( - 'backup_controller_page_filename', - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr( - namedArgs: isUploadInProgress - ? {'filename': asset.fileName, 'size': asset.fileType.toLowerCase()} - : {'filename': "-", 'size': "-"}, - ), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - verticalAlignment: TableCellVerticalAlignment.middle, - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - "backup_controller_page_created", - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr(namedArgs: {'date': isUploadInProgress ? _getAssetCreationDate(asset) : "-"}), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - "backup_controller_page_id", - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr(namedArgs: {'id': isUploadInProgress ? asset.id : "-"}), - ), - ), - ], - ), - ], - ), - ); - } - - @pragma('vm:prefer-inline') - String _getAssetCreationDate(CurrentUploadAsset asset) { - return DateFormat.yMMMMd().format(asset.fileCreatedAt.toLocal()); - } -} diff --git a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart deleted file mode 100644 index c2f94e706a..0000000000 --- a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/backup/asset_info_table.dart'; -import 'package:immich_mobile/widgets/backup/error_chip.dart'; -import 'package:immich_mobile/widgets/backup/icloud_download_progress_bar.dart'; -import 'package:immich_mobile/widgets/backup/upload_progress_bar.dart'; -import 'package:immich_mobile/widgets/backup/upload_stats.dart'; - -class CurrentUploadingAssetInfoBox extends StatelessWidget { - const CurrentUploadingAssetInfoBox({super.key}); - - @override - Widget build(BuildContext context) { - return ListTile( - isThreeLine: true, - leading: Icon(Icons.image_outlined, color: context.primaryColor, size: 30), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("backup_controller_page_uploading_file_info", style: context.textTheme.titleSmall).tr(), - const BackupErrorChip(), - ], - ), - subtitle: Column( - children: [ - if (Platform.isIOS) const IcloudDownloadProgressBar(), - const BackupUploadProgressBar(), - const BackupUploadStats(), - const BackupAssetInfoTable(), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/error_chip.dart b/mobile/lib/widgets/backup/error_chip.dart deleted file mode 100644 index 191049cd75..0000000000 --- a/mobile/lib/widgets/backup/error_chip.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/backup/error_chip_text.dart'; - -class BackupErrorChip extends ConsumerWidget { - const BackupErrorChip({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final hasErrors = ref.watch(errorBackupListProvider.select((value) => value.isNotEmpty)); - if (!hasErrors) { - return const SizedBox(); - } - - return ActionChip( - avatar: const Icon(Icons.info, color: red400), - elevation: 1, - visualDensity: VisualDensity.compact, - label: const BackupErrorChipText(), - backgroundColor: Colors.white, - onPressed: () => context.pushRoute(const FailedBackupStatusRoute()), - ); - } -} diff --git a/mobile/lib/widgets/backup/error_chip_text.dart b/mobile/lib/widgets/backup/error_chip_text.dart deleted file mode 100644 index c987dfd331..0000000000 --- a/mobile/lib/widgets/backup/error_chip_text.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; - -class BackupErrorChipText extends ConsumerWidget { - const BackupErrorChipText({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final count = ref.watch(errorBackupListProvider).length; - if (count == 0) { - return const SizedBox(); - } - - return const Text( - "backup_controller_page_failed", - style: TextStyle(color: red400, fontWeight: FontWeight.bold, fontSize: 11), - ).tr(namedArgs: {'count': count.toString()}); - } -} diff --git a/mobile/lib/widgets/backup/icloud_download_progress_bar.dart b/mobile/lib/widgets/backup/icloud_download_progress_bar.dart deleted file mode 100644 index 9f0f7ec3eb..0000000000 --- a/mobile/lib/widgets/backup/icloud_download_progress_bar.dart +++ /dev/null @@ -1,43 +0,0 @@ -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/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class IcloudDownloadProgressBar extends ConsumerWidget { - const IcloudDownloadProgressBar({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final isIcloudAsset = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset)) - : ref.watch(backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset)); - - if (!isIcloudAsset) { - return const SizedBox(); - } - - final iCloudDownloadProgress = ref.watch(backupProvider.select((value) => value.iCloudDownloadProgress)); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - children: [ - SizedBox(width: 110, child: Text("iCloud Download", style: context.textTheme.labelSmall)), - Expanded( - child: LinearProgressIndicator( - minHeight: 10.0, - value: iCloudDownloadProgress / 100.0, - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - ), - ), - Text(" ${iCloudDownloadProgress ~/ 1}%", style: const TextStyle(fontSize: 12)), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/ios_debug_info_tile.dart b/mobile/lib/widgets/backup/ios_debug_info_tile.dart deleted file mode 100644 index be333c6460..0000000000 --- a/mobile/lib/widgets/backup/ios_debug_info_tile.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:intl/intl.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; - -/// This is a simple debug widget which should be removed later on when we are -/// more confident about background sync -class IosDebugInfoTile extends HookConsumerWidget { - final IOSBackgroundSettings settings; - const IosDebugInfoTile({super.key, required this.settings}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final fetch = settings.timeOfLastFetch; - final processing = settings.timeOfLastProcessing; - final processes = settings.numberOfBackgroundTasksQueued; - - final String title; - if (processes == 0) { - title = 'ios_debug_info_no_processes_queued'.t(context: context); - } else { - title = 'ios_debug_info_processes_queued'.t(context: context, args: {'count': processes}); - } - - final df = DateFormat.yMd().add_jm(); - final String subtitle; - if (fetch == null && processing == null) { - subtitle = 'ios_debug_info_no_sync_yet'.t(context: context); - } else if (fetch != null && processing == null) { - subtitle = 'ios_debug_info_fetch_ran_at'.t(context: context, args: {'dateTime': df.format(fetch)}); - } else if (processing != null && fetch == null) { - subtitle = 'ios_debug_info_processing_ran_at'.t(context: context, args: {'dateTime': df.format(processing)}); - } else { - final fetchOrProcessing = fetch!.isAfter(processing!) ? fetch : processing; - subtitle = 'ios_debug_info_last_sync_at'.t(context: context, args: {'dateTime': df.format(fetchOrProcessing)}); - } - - return ListTile( - title: Text( - title, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: context.primaryColor), - ), - subtitle: Text(subtitle, style: const TextStyle(fontSize: 14)), - leading: Icon(Icons.bug_report, color: context.primaryColor), - ); - } -} diff --git a/mobile/lib/widgets/backup/upload_progress_bar.dart b/mobile/lib/widgets/backup/upload_progress_bar.dart deleted file mode 100644 index 641ed14878..0000000000 --- a/mobile/lib/widgets/backup/upload_progress_bar.dart +++ /dev/null @@ -1,45 +0,0 @@ -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/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class BackupUploadProgressBar extends ConsumerWidget { - const BackupUploadProgressBar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final isIcloudAsset = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset)) - : ref.watch(backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset)); - - final uploadProgress = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.progressInPercentage)) - : ref.watch(backupProvider.select((value) => value.progressInPercentage)); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - children: [ - if (isIcloudAsset) SizedBox(width: 110, child: Text("Immich Upload", style: context.textTheme.labelSmall)), - Expanded( - child: LinearProgressIndicator( - minHeight: 10.0, - value: uploadProgress / 100.0, - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - ), - ), - Text( - " ${uploadProgress.toStringAsFixed(0)}%", - style: const TextStyle(fontSize: 12, fontFamily: "GoogleSansCode"), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/upload_stats.dart b/mobile/lib/widgets/backup/upload_stats.dart deleted file mode 100644 index 38f99e53fc..0000000000 --- a/mobile/lib/widgets/backup/upload_stats.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class BackupUploadStats extends ConsumerWidget { - const BackupUploadStats({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final uploadFileProgress = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.progressInFileSize)) - : ref.watch(backupProvider.select((value) => value.progressInFileSize)); - - final uploadFileSpeed = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.progressInFileSpeed)) - : ref.watch(backupProvider.select((value) => value.progressInFileSpeed)); - - return Padding( - padding: const EdgeInsets.only(top: 2.0, bottom: 2.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(uploadFileProgress, style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode")), - Text( - _formatUploadFileSpeed(uploadFileSpeed), - style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode"), - ), - ], - ), - ); - } - - @pragma('vm:prefer-inline') - String _formatUploadFileSpeed(double uploadFileSpeed) { - if (uploadFileSpeed < 1024) { - return '${uploadFileSpeed.toStringAsFixed(2)} B/s'; - } else if (uploadFileSpeed < 1024 * 1024) { - return '${(uploadFileSpeed / 1024).toStringAsFixed(2)} KB/s'; - } else if (uploadFileSpeed < 1024 * 1024 * 1024) { - return '${(uploadFileSpeed / (1024 * 1024)).toStringAsFixed(2)} MB/s'; - } else { - return '${(uploadFileSpeed / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB/s'; - } - } -} 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 c330fb4649..e77bc1869e 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 @@ -5,18 +5,15 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; +import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_profile_info.dart'; @@ -32,7 +29,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { ref.watch(localeProvider); - BackUpState backupState = ref.watch(backupProvider); + ServerDiskInfo backupState = ref.watch(backupProvider); final theme = context.themeData; bool isHorizontal = !context.isMobile; final horizontalPadding = isHorizontal ? 100.0 : 20.0; @@ -53,7 +50,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { alignment: Alignment.centerLeft, children: [ IconButton( - onPressed: () => context.pop(), + onPressed: () => ContextHelper(context).pop(), icon: Icon(Icons.close, size: 20, color: context.colorScheme.onSurfaceVariant), ), Align( @@ -128,9 +125,6 @@ class ImmichAppBarDialog extends HookConsumerWidget { isLoggingOut.value = true; await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false); - ref.read(manualUploadProvider.notifier).cancelBackup(); - ref.read(backupProvider.notifier).cancelBackup(); - unawaited(ref.read(assetProvider.notifier).clearAllAssets()); ref.read(websocketProvider.notifier).disconnect(); unawaited(context.replaceRoute(const LoginRoute())); }, @@ -146,9 +140,9 @@ class ImmichAppBarDialog extends HookConsumerWidget { } Widget buildStorageInformation() { - var percentage = backupState.serverInfo.diskUsagePercentage / 100; - var usedDiskSpace = backupState.serverInfo.diskUse; - var totalDiskSpace = backupState.serverInfo.diskSize; + var percentage = backupState.diskUsagePercentage / 100; + var usedDiskSpace = backupState.diskUse; + var totalDiskSpace = backupState.diskSize; if (user != null && user.hasQuota) { usedDiskSpace = formatBytes(user.quotaUsageInBytes); @@ -185,7 +179,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { children: [ InkWell( onTap: () { - context.pop(); + ContextHelper(context).pop(); launchUrl(Uri.parse('https://docs.immich.app'), mode: LaunchMode.externalApplication); }, child: Text("documentation", style: context.textTheme.bodySmall).tr(), @@ -193,7 +187,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { const SizedBox(width: 20, child: Text("â€ĸ", textAlign: TextAlign.center)), InkWell( onTap: () { - context.pop(); + ContextHelper(context).pop(); launchUrl(Uri.parse('https://github.com/immich-app/immich'), mode: LaunchMode.externalApplication); }, child: Text("profile_drawer_github", style: context.textTheme.bodySmall).tr(), @@ -201,7 +195,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { const SizedBox(width: 20, child: Text("â€ĸ", textAlign: TextAlign.center)), InkWell( onTap: () async { - context.pop(); + ContextHelper(context).pop(); final packageInfo = await PackageInfo.fromPlatform(); showLicensePage( context: context, @@ -241,7 +235,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { return Dismissible( behavior: HitTestBehavior.translucent, direction: DismissDirection.down, - onDismissed: (_) => context.pop(), + onDismissed: (_) => ContextHelper(context).pop(), key: const Key('app_bar_dialog'), child: Dialog( clipBehavior: Clip.hardEdge, @@ -275,7 +269,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ], ), ), - if (Store.isBetaTimelineEnabled && isReadonlyModeEnabled) buildReadonlyMessage(), + if (isReadonlyModeEnabled) buildReadonlyMessage(), buildAppLogButton(), buildFreeUpSpaceButton(), buildSettingButton(), 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 a9fdb9a43f..d6881f519a 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 @@ -4,7 +4,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; @@ -62,10 +61,6 @@ class AppBarProfileInfoBox extends HookConsumerWidget { } void toggleReadonlyMode() { - // read only mode is only supported int he beta experience - // TODO: remove this check when the beta UI goes stable - if (!Store.isBetaTimelineEnabled) return; - final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); ref.read(readonlyModeProvider.notifier).toggleReadonlyMode(); diff --git a/mobile/lib/widgets/common/drag_sheet.dart b/mobile/lib/widgets/common/drag_sheet.dart deleted file mode 100644 index 5d1fda1beb..0000000000 --- a/mobile/lib/widgets/common/drag_sheet.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class CustomDraggingHandle extends StatelessWidget { - const CustomDraggingHandle({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - height: 4, - width: 30, - decoration: BoxDecoration( - color: context.themeData.dividerColor, - borderRadius: const BorderRadius.all(Radius.circular(20)), - ), - ); - } -} - -class ControlBoxButton extends StatelessWidget { - const ControlBoxButton({super.key, required this.label, required this.iconData, this.onPressed, this.onLongPressed}); - - final String label; - final IconData iconData; - final void Function()? onPressed; - final void Function()? onLongPressed; - - @override - Widget build(BuildContext context) { - final minWidth = context.isMobile ? MediaQuery.sizeOf(context).width / 4.5 : 75.0; - - return MaterialButton( - padding: const EdgeInsets.all(10), - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))), - onPressed: onPressed, - onLongPress: onLongPressed, - minWidth: minWidth, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(iconData, size: 24), - const SizedBox(height: 8), - Text( - label, - style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400), - maxLines: 3, - textAlign: TextAlign.center, - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart deleted file mode 100644 index 56b7e91eec..0000000000 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.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'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; -import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_dialog.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); - final List? actions; - final bool showUploadButton; - - const ImmichAppBar({super.key, this.actions, this.showUploadButton = true}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final BackUpState backupState = ref.watch(backupProvider); - final bool isEnableAutoBackup = backupState.backgroundBackup || backupState.autoBackup; - final user = ref.watch(currentUserProvider); - final bool versionWarningPresent = ref.watch(versionWarningPresentProvider(user)); - final isDarkTheme = context.isDarkTheme; - const widgetSize = 30.0; - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - buildProfileIndicator() { - return InkWell( - onTap: () => - showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Badge( - label: Container( - decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(widgetSize / 2)), - child: const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: widgetSize / 2), - ), - backgroundColor: Colors.transparent, - alignment: Alignment.bottomRight, - isLabelVisible: versionWarningPresent, - offset: const Offset(-2, -12), - child: user == null - ? const Icon(Icons.face_outlined, size: widgetSize) - : Semantics( - label: "logged_in_as".tr(namedArgs: {"user": user.name}), - child: UserCircleAvatar(size: 32, user: user), - ), - ), - ); - } - - getBackupBadgeIcon() { - final iconColor = isDarkTheme ? Colors.white : Colors.black; - - if (isEnableAutoBackup) { - if (backupState.backupProgress == BackUpProgressEnum.inProgress) { - return Container( - padding: const EdgeInsets.all(3.5), - child: CircularProgressIndicator( - strokeWidth: 2, - strokeCap: StrokeCap.round, - valueColor: AlwaysStoppedAnimation(iconColor), - semanticsLabel: 'backup_controller_page_backup'.tr(), - ), - ); - } else if (backupState.backupProgress != BackUpProgressEnum.inBackground && - backupState.backupProgress != BackUpProgressEnum.manualInProgress) { - return Icon( - Icons.check_outlined, - size: 9, - color: iconColor, - semanticLabel: 'backup_controller_page_backup'.tr(), - ); - } - } - - if (!isEnableAutoBackup) { - return Icon( - Icons.cloud_off_rounded, - size: 9, - color: iconColor, - semanticLabel: 'backup_controller_page_backup'.tr(), - ); - } - } - - buildBackupIndicator() { - final indicatorIcon = getBackupBadgeIcon(); - final badgeBackground = context.colorScheme.surfaceContainer; - - return InkWell( - onTap: () => context.pushRoute(const BackupControllerRoute()), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Badge( - label: Container( - width: widgetSize / 2, - height: widgetSize / 2, - decoration: BoxDecoration( - color: badgeBackground, - border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3)), - borderRadius: BorderRadius.circular(widgetSize / 2), - ), - child: indicatorIcon, - ), - backgroundColor: Colors.transparent, - alignment: Alignment.bottomRight, - isLabelVisible: indicatorIcon != null, - offset: const Offset(-2, -12), - child: Icon(Icons.backup_rounded, size: widgetSize, color: context.primaryColor), - ), - ); - } - - return AppBar( - backgroundColor: context.themeData.appBarTheme.backgroundColor, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - automaticallyImplyLeading: false, - centerTitle: false, - title: Builder( - builder: (BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(top: 3.0), - child: SvgPicture.asset( - context.isDarkTheme ? 'assets/immich-logo-inline-dark.svg' : 'assets/immich-logo-inline-light.svg', - height: 40, - ), - ), - const Tooltip( - triggerMode: TooltipTriggerMode.tap, - showDuration: Duration(seconds: 4), - message: - "The old timeline is deprecated and will be removed in a future release. Kindly switch to the new timeline under Advanced Settings.", - child: Padding( - padding: EdgeInsets.only(top: 3.0), - child: Icon(Icons.error_rounded, fill: 1, color: Colors.amber, size: 20), - ), - ), - ], - ); - }, - ), - actions: [ - if (actions != null) - ...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)), - if (isCasting) - Padding( - padding: const EdgeInsets.only(right: 12), - child: IconButton( - onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); - }, - icon: Icon(isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded), - ), - ), - if (showUploadButton) Padding(padding: const EdgeInsets.only(right: 20), child: buildBackupIndicator()), - Padding(padding: const EdgeInsets.only(right: 20), child: buildProfileIndicator()), - ], - ); - } -} diff --git a/mobile/lib/widgets/common/immich_image.dart b/mobile/lib/widgets/common/immich_image.dart deleted file mode 100644 index 57978e83ff..0000000000 --- a/mobile/lib/widgets/common/immich_image.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:octo_image/octo_image.dart'; - -class ImmichImage extends StatelessWidget { - const ImmichImage( - this.asset, { - this.width, - this.height, - this.fit = BoxFit.cover, - this.placeholder = const ThumbnailPlaceholder(), - super.key, - }); - - final Asset? asset; - final Widget? placeholder; - final double? width; - final double? height; - final BoxFit fit; - - // Helper function to return the image provider for the asset - // either by using the asset ID or the asset itself - /// [asset] is the Asset to request, or else use [assetId] to get a remote - /// image provider - static ImageProvider imageProvider({Asset? asset, String? assetId, double width = 1080, double height = 1920}) { - if (asset == null && assetId == null) { - throw Exception('Must supply either asset or assetId'); - } - - if (asset == null) { - return RemoteFullImageProvider( - assetId: assetId!, - thumbhash: '', - assetType: base_asset.AssetType.video, - isAnimated: false, - ); - } - - if (useLocal(asset)) { - return LocalFullImageProvider( - id: asset.localId!, - assetType: base_asset.AssetType.video, - size: Size(width, height), - isAnimated: false, - ); - } else { - return RemoteFullImageProvider( - assetId: asset.remoteId!, - thumbhash: asset.thumbhash ?? '', - assetType: base_asset.AssetType.video, - isAnimated: false, - ); - } - } - - // Whether to use the local asset image provider or a remote one - static bool useLocal(Asset asset) => - !asset.isRemote || asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false); - - @override - Widget build(BuildContext context) { - if (asset == null) { - return Container( - color: Colors.grey, - width: width, - height: height, - child: const Center(child: Icon(Icons.no_photography)), - ); - } - - final imageProviderInstance = ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height); - - return OctoImage( - fadeInDuration: const Duration(milliseconds: 0), - fadeOutDuration: const Duration(milliseconds: 100), - placeholderBuilder: (context) { - if (placeholder != null) { - return placeholder!; - } - return const SizedBox(); - }, - image: imageProviderInstance, - width: width, - height: height, - fit: fit, - errorBuilder: (context, error, stackTrace) { - imageProviderInstance.evict(); - - return Icon(Icons.image_not_supported_outlined, size: 32, color: Colors.red[200]); - }, - ); - } -} diff --git a/mobile/lib/widgets/common/immich_thumbnail.dart b/mobile/lib/widgets/common/immich_thumbnail.dart deleted file mode 100644 index f17353c3aa..0000000000 --- a/mobile/lib/widgets/common/immich_thumbnail.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; -import 'package:immich_mobile/utils/thumbnail_utils.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart'; -import 'package:octo_image/octo_image.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; - -class ImmichThumbnail extends HookConsumerWidget { - const ImmichThumbnail({this.asset, this.width = 250, this.height = 250, this.fit = BoxFit.cover, super.key}); - - final Asset? asset; - final double width; - final double height; - final BoxFit fit; - - /// Helper function to return the image provider for the asset thumbnail - /// either by using the asset ID or the asset itself - /// [asset] is the Asset to request, or else use [assetId] to get a remote - /// image provider - static ImageProvider imageProvider({Asset? asset, String? assetId, int thumbnailSize = 256}) { - if (asset == null && assetId == null) { - throw Exception('Must supply either asset or assetId'); - } - - if (asset == null) { - return RemoteImageProvider.thumbnail(assetId: assetId!, thumbhash: ""); - } - - if (ImmichImage.useLocal(asset)) { - return LocalThumbProvider( - id: asset.localId!, - assetType: base_asset.AssetType.video, - size: Size(thumbnailSize.toDouble(), thumbnailSize.toDouble()), - ); - } else { - return RemoteImageProvider.thumbnail(assetId: asset.remoteId!, thumbhash: asset.thumbhash ?? ""); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - Uint8List? blurhash = useBlurHashRef(asset).value; - - if (asset == null) { - return Container( - color: Colors.grey, - width: width, - height: height, - child: const Center(child: Icon(Icons.no_photography)), - ); - } - - final assetAltText = getAltText(asset!.exifInfo, asset!.fileCreatedAt, asset!.type, []); - - final thumbnailProviderInstance = ImmichThumbnail.imageProvider(asset: asset); - - customErrorBuilder(BuildContext ctx, Object error, StackTrace? stackTrace) { - thumbnailProviderInstance.evict(); - - final originalErrorWidgetBuilder = blurHashErrorBuilder(blurhash, fit: fit); - return originalErrorWidgetBuilder(ctx, error, stackTrace); - } - - return Semantics( - label: assetAltText, - child: OctoImage.fromSet( - placeholderFadeInDuration: Duration.zero, - fadeInDuration: Duration.zero, - fadeOutDuration: const Duration(milliseconds: 100), - octoSet: OctoSet( - placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), - errorBuilder: customErrorBuilder, - ), - image: thumbnailProviderInstance, - width: width, - height: height, - fit: fit, - ), - ); - } -} diff --git a/mobile/lib/widgets/common/immich_toast.dart b/mobile/lib/widgets/common/immich_toast.dart index dad8b33283..3e7ab273d8 100644 --- a/mobile/lib/widgets/common/immich_toast.dart +++ b/mobile/lib/widgets/common/immich_toast.dart @@ -55,7 +55,7 @@ class ImmichToast { bottom: gravity == ToastGravity.BOTTOM ? 150 : null, left: MediaQuery.of(context).size.width / 2 - 150, right: MediaQuery.of(context).size.width / 2 - 150, - child: child, + child: IgnorePointer(child: child), ); }, gravity: gravity, diff --git a/mobile/lib/widgets/common/location_picker.dart b/mobile/lib/widgets/common/location_picker.dart index 4736b182ed..c7eb827781 100644 --- a/mobile/lib/widgets/common/location_picker.dart +++ b/mobile/lib/widgets/common/location_picker.dart @@ -107,7 +107,7 @@ class _LocationPicker extends HookWidget { ), actions: [ TextButton( - onPressed: () => context.pop(), + onPressed: () => ContextHelper(context).pop(), child: Text( "cancel", style: context.textTheme.bodyMedium?.copyWith( diff --git a/mobile/lib/widgets/common/share_dialog.dart b/mobile/lib/widgets/common/share_dialog.dart deleted file mode 100644 index 625390c4b7..0000000000 --- a/mobile/lib/widgets/common/share_dialog.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class ShareDialog extends StatelessWidget { - const ShareDialog({super.key}); - - @override - Widget build(BuildContext context) { - return AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator(), - Container(margin: const EdgeInsets.only(top: 12), child: const Text('share_dialog_preparing').tr()), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/common/thumbhash_placeholder.dart b/mobile/lib/widgets/common/thumbhash_placeholder.dart index 0cb1222989..8a9c2eb928 100644 --- a/mobile/lib/widgets/common/thumbhash_placeholder.dart +++ b/mobile/lib/widgets/common/thumbhash_placeholder.dart @@ -4,15 +4,6 @@ import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; import 'package:immich_mobile/widgets/common/fade_in_placeholder_image.dart'; import 'package:octo_image/octo_image.dart'; -/// Simple set to show [OctoPlaceholder.circularProgressIndicator] as -/// placeholder and [OctoError.icon] as error. -OctoSet blurHashOrPlaceholder(Uint8List? blurhash, {BoxFit? fit, Text? errorMessage}) { - return OctoSet( - placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), - errorBuilder: blurHashErrorBuilder(blurhash, fit: fit, message: errorMessage), - ); -} - OctoPlaceholderBuilder blurHashPlaceholderBuilder(Uint8List? blurhash, {BoxFit? fit}) { return (context) => blurhash == null ? const ThumbnailPlaceholder() diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index 179b05a712..7ed9fa5f1c 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.dart @@ -1,14 +1,11 @@ -import 'package:easy_localization/easy_localization.dart'; 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:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -64,10 +61,6 @@ class ChangePasswordForm extends HookConsumerWidget { if (isSuccess) { await ref.read(authProvider.notifier).logout(); - - ref.read(manualUploadProvider.notifier).cancelBackup(); - ref.read(backupProvider.notifier).cancelBackup(); - await ref.read(assetProvider.notifier).clearAllAssets(); ref.read(websocketProvider.notifier).disconnect(); AutoRouter.of(context).back(); diff --git a/mobile/lib/widgets/forms/login/email_input.dart b/mobile/lib/widgets/forms/login/email_input.dart deleted file mode 100644 index 4d90d918ac..0000000000 --- a/mobile/lib/widgets/forms/login/email_input.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class EmailInput extends StatelessWidget { - final TextEditingController controller; - final FocusNode? focusNode; - final Function()? onSubmit; - - const EmailInput({super.key, required this.controller, this.focusNode, this.onSubmit}); - - String? _validateInput(String? email) { - if (email == null || email == '') return null; - if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr(); - if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr(); - if (email.contains(' ') || !email.contains('@')) { - return 'login_form_err_invalid_email'.tr(); - } - return null; - } - - @override - Widget build(BuildContext context) { - return TextFormField( - autofocus: true, - controller: controller, - decoration: InputDecoration( - labelText: 'email'.tr(), - border: const OutlineInputBorder(), - hintText: 'login_form_email_hint'.tr(), - hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14), - ), - validator: _validateInput, - autovalidateMode: AutovalidateMode.always, - autofillHints: const [AutofillHints.email], - keyboardType: TextInputType.emailAddress, - onFieldSubmitted: (_) => onSubmit?.call(), - focusNode: focusNode, - textInputAction: TextInputAction.next, - ); - } -} diff --git a/mobile/lib/widgets/forms/login/loading_icon.dart b/mobile/lib/widgets/forms/login/loading_icon.dart deleted file mode 100644 index 052ce43ac7..0000000000 --- a/mobile/lib/widgets/forms/login/loading_icon.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/material.dart'; - -class LoadingIcon extends StatelessWidget { - const LoadingIcon({super.key}); - - @override - Widget build(BuildContext context) { - return const Padding( - padding: EdgeInsets.only(top: 18.0), - child: SizedBox(width: 24, height: 24, child: FittedBox(child: CircularProgressIndicator(strokeWidth: 2))), - ); - } -} diff --git a/mobile/lib/widgets/forms/login/login_button.dart b/mobile/lib/widgets/forms/login/login_button.dart deleted file mode 100644 index 0f9fb21d8f..0000000000 --- a/mobile/lib/widgets/forms/login/login_button.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -class LoginButton extends ConsumerWidget { - final Function() onPressed; - - const LoginButton({super.key, required this.onPressed}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ElevatedButton.icon( - style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)), - onPressed: onPressed, - icon: const Icon(Icons.login_rounded), - label: const Text("login", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ); - } -} diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 2aa770f104..fb3b9c5977 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -17,7 +17,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/oauth.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -34,7 +33,6 @@ import 'package:immich_ui/immich_ui.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:permission_handler/permission_handler.dart'; class LoginForm extends HookConsumerWidget { LoginForm({super.key}); @@ -246,18 +244,14 @@ class LoginForm extends HookConsumerWidget { if (result.shouldChangePassword && !result.isAdmin) { unawaited(context.pushRoute(const ChangePasswordRoute())); } else { - final isBeta = Store.isBetaTimelineEnabled; - if (isBeta) { - await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - if (isSyncRemoteDeletionsMode()) { - await getManageMediaPermission(); - } - unawaited(handleSyncFlow()); - ref.read(websocketProvider.notifier).connect(); - unawaited(context.replaceRoute(const TabShellRoute())); - return; + await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + if (isSyncRemoteDeletionsMode()) { + await getManageMediaPermission(); } - unawaited(context.replaceRoute(const TabControllerRoute())); + unawaited(handleSyncFlow()); + ref.read(websocketProvider.notifier).connect(); + unawaited(context.replaceRoute(const TabShellRoute())); + return; } } catch (error) { ImmichToast.show( @@ -338,21 +332,13 @@ class LoginForm extends HookConsumerWidget { .saveAuthInfo(accessToken: loginResponseDto.accessToken); if (isSuccess) { - final permission = ref.watch(galleryPermissionNotifier); - final isBeta = Store.isBetaTimelineEnabled; - if (!isBeta && (permission.isGranted || permission.isLimited)) { - unawaited(ref.watch(backupProvider.notifier).resumeBackup()); + await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + if (isSyncRemoteDeletionsMode()) { + await getManageMediaPermission(); } - if (isBeta) { - await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - if (isSyncRemoteDeletionsMode()) { - await getManageMediaPermission(); - } - unawaited(handleSyncFlow()); - unawaited(context.replaceRoute(const TabShellRoute())); - return; - } - unawaited(context.replaceRoute(const TabControllerRoute())); + unawaited(handleSyncFlow()); + unawaited(context.replaceRoute(const TabShellRoute())); + return; } } catch (error, stack) { log.severe('Error logging in with OAuth: $error', stack); diff --git a/mobile/lib/widgets/map/map_app_bar.dart b/mobile/lib/widgets/map/map_app_bar.dart deleted file mode 100644 index 73706c7661..0000000000 --- a/mobile/lib/widgets/map/map_app_bar.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'dart:async'; - -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/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/widgets/map/map_settings_sheet.dart'; - -class MapAppBar extends HookWidget implements PreferredSizeWidget { - final ValueNotifier> selectedAssets; - - const MapAppBar({super.key, required this.selectedAssets}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only(top: context.padding.top + 25), - child: ValueListenableBuilder( - valueListenable: selectedAssets, - builder: (ctx, value, child) => - value.isNotEmpty ? _SelectionRow(selectedAssets: selectedAssets) : const _NonSelectionRow(), - ), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(100); -} - -class _NonSelectionRow extends StatelessWidget { - const _NonSelectionRow(); - - @override - Widget build(BuildContext context) { - void onSettingsPressed() { - showModalBottomSheet( - elevation: 0.0, - showDragHandle: true, - isScrollControlled: true, - context: context, - builder: (_) => const MapSettingsSheet(), - ); - } - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton( - onPressed: () => context.maybePop(), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.arrow_back_ios_new_rounded), - ), - ElevatedButton( - onPressed: onSettingsPressed, - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.more_vert_rounded), - ), - ], - ); - } -} - -class _SelectionRow extends HookConsumerWidget { - final ValueNotifier> selectedAssets; - - const _SelectionRow({required this.selectedAssets}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isProcessing = useProcessingOverlay(); - - Future handleProcessing(FutureOr Function() action, [bool reloadMarkers = false]) async { - isProcessing.value = true; - await action(); - // Reset state - selectedAssets.value = {}; - isProcessing.value = false; - if (reloadMarkers) { - ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(true); - } - } - - return Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 20), - child: ElevatedButton.icon( - onPressed: () => selectedAssets.value = {}, - icon: const Icon(Icons.close_rounded), - label: Text( - '${selectedAssets.value.length}', - style: context.textTheme.titleMedium?.copyWith(color: context.colorScheme.onPrimary), - ), - ), - ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton( - onPressed: () => handleProcessing(() => handleShareAssets(ref, context, selectedAssets.value.toList())), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.ios_share_rounded), - ), - ElevatedButton( - onPressed: () => - handleProcessing(() => handleFavoriteAssets(ref, context, selectedAssets.value.toList())), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.favorite), - ), - ElevatedButton( - onPressed: () => - handleProcessing(() => handleArchiveAssets(ref, context, selectedAssets.value.toList()), true), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.archive), - ), - ], - ), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/map/map_asset_grid.dart b/mobile/lib/widgets/map/map_asset_grid.dart deleted file mode 100644 index b6c1e708a7..0000000000 --- a/mobile/lib/widgets/map/map_asset_grid.dart +++ /dev/null @@ -1,289 +0,0 @@ -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/utils/color_filter_generator.dart'; -import 'package:immich_mobile/utils/throttle.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/widgets/common/drag_sheet.dart'; -import 'package:logging/logging.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -class MapAssetGrid extends HookConsumerWidget { - final Stream mapEventStream; - final Function(String)? onGridAssetChanged; - final Function(String)? onZoomToAsset; - final Function(bool, Set)? onAssetsSelected; - final ValueNotifier> selectedAssets; - final ScrollController controller; - - const MapAssetGrid({ - required this.mapEventStream, - this.onGridAssetChanged, - this.onZoomToAsset, - this.onAssetsSelected, - required this.selectedAssets, - required this.controller, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final log = Logger("MapAssetGrid"); - final assetsInBounds = useState>([]); - final cachedRenderList = useRef(null); - final lastRenderElementIndex = useRef(null); - final assetInSheet = useValueNotifier(null); - final gridScrollThrottler = useThrottler(interval: const Duration(milliseconds: 300)); - - // Add a cache for assets we've already loaded - final assetCache = useRef>({}); - - void handleMapEvents(MapEvent event) async { - if (event is MapAssetsInBoundsUpdated) { - final assetIds = event.assetRemoteIds; - final missingIds = []; - final currentAssets = []; - - for (final id in assetIds) { - final asset = assetCache.value[id]; - if (asset != null) { - currentAssets.add(asset); - } else { - missingIds.add(id); - } - } - - // Only fetch missing assets - if (missingIds.isNotEmpty) { - final newAssets = await ref.read(dbProvider).assets.getAllByRemoteId(missingIds); - - // Add new assets to cache and current list - for (final asset in newAssets) { - if (asset.remoteId != null) { - assetCache.value[asset.remoteId!] = asset; - currentAssets.add(asset); - } - } - } - - assetsInBounds.value = currentAssets; - return; - } - } - - useOnStreamChange(mapEventStream, onData: handleMapEvents); - - // Hard-restrict to 4 assets / row in portrait mode - const assetsPerRow = 4; - - void handleVisibleItems(Iterable positions) { - final orderedPos = positions.sortedByField((p) => p.index); - // Index of row where the items are mostly visible - const partialOffset = 0.20; - final item = orderedPos.firstWhereOrNull((p) => p.itemTrailingEdge > partialOffset); - - // Guard no elements, reset state - // Also fail fast when the sheet is just opened and the user is yet to scroll (i.e leading = 0) - if (item == null || item.itemLeadingEdge == 0) { - lastRenderElementIndex.value = null; - return; - } - - final renderElement = cachedRenderList.value?.elements.elementAtOrNull(item.index); - // Guard no render list or render element - if (renderElement == null) { - return; - } - // Reset index - lastRenderElementIndex.value == item.index; - - // - // | 1 | 2 | 3 | 4 | 5 | 6 | - // - // | 7 | 8 | 9 | - // - // | 10 | - - // Skip through the assets from the previous row - final rowOffset = renderElement.offset; - // Column offset = (total trailingEdge - trailingEdge crossed) / offset for each asset - final totalOffset = item.itemTrailingEdge - item.itemLeadingEdge; - final edgeOffset = - (totalOffset - partialOffset) / - // Round the total count to the next multiple of [assetsPerRow] - ((renderElement.totalCount / assetsPerRow) * assetsPerRow).floor(); - - // trailing should never be above the totalOffset - final columnOffset = (totalOffset - math.min(item.itemTrailingEdge, totalOffset)) ~/ edgeOffset; - final assetOffset = rowOffset + columnOffset; - final selectedAsset = cachedRenderList.value?.allAssets?.elementAtOrNull(assetOffset)?.remoteId; - - if (selectedAsset != null) { - onGridAssetChanged?.call(selectedAsset); - assetInSheet.value = selectedAsset; - } - } - - return Card( - margin: EdgeInsets.zero, - child: Stack( - children: [ - /// The Align and FractionallySizedBox are to prevent the Asset Grid from going behind the - /// _MapSheetDragRegion and thereby displaying content behind the top right and top left curves - Align( - alignment: Alignment.bottomCenter, - child: FractionallySizedBox( - // Place it just below the drag handle - heightFactor: 0.87, - child: assetsInBounds.value.isNotEmpty - ? ref - .watch(assetsTimelineProvider(assetsInBounds.value)) - .when( - data: (renderList) { - // Cache render list here to use it back during visibleItemsListener - cachedRenderList.value = renderList; - return ValueListenableBuilder( - valueListenable: selectedAssets, - builder: (_, value, __) => ImmichAssetGrid( - shrinkWrap: true, - renderList: renderList, - showDragScroll: false, - assetsPerRow: assetsPerRow, - showMultiSelectIndicator: false, - selectionActive: value.isNotEmpty, - listener: onAssetsSelected, - visibleItemsListener: (pos) => gridScrollThrottler.run(() => handleVisibleItems(pos)), - ), - ); - }, - error: (error, stackTrace) { - log.warning("Cannot get assets in the current map bounds", error, stackTrace); - return const SizedBox.shrink(); - }, - loading: () => const SizedBox.shrink(), - ) - : const _MapNoAssetsInSheet(), - ), - ), - _MapSheetDragRegion( - controller: controller, - assetsInBoundCount: assetsInBounds.value.length, - assetInSheet: assetInSheet, - onZoomToAsset: onZoomToAsset, - ), - ], - ), - ); - } -} - -class _MapNoAssetsInSheet extends StatelessWidget { - const _MapNoAssetsInSheet(); - - @override - Widget build(BuildContext context) { - const image = Image(height: 150, width: 150, image: AssetImage('assets/lighthouse.png')); - - return Center( - child: ListView( - shrinkWrap: true, - children: [ - context.isDarkTheme - ? const InvertionFilter( - child: SaturationFilter(saturation: -1, child: BrightnessFilter(brightness: -5, child: image)), - ) - : image, - const SizedBox(height: 20), - Center( - child: Text("map_zoom_to_see_photos".tr(), style: context.textTheme.displayLarge?.copyWith(fontSize: 18)), - ), - ], - ), - ); - } -} - -class _MapSheetDragRegion extends StatelessWidget { - final ScrollController controller; - final int assetsInBoundCount; - final ValueNotifier assetInSheet; - final Function(String)? onZoomToAsset; - - const _MapSheetDragRegion({ - required this.controller, - required this.assetsInBoundCount, - required this.assetInSheet, - this.onZoomToAsset, - }); - - @override - Widget build(BuildContext context) { - final assetsInBoundsText = "map_assets_in_bounds".t(context: context, args: {'count': assetsInBoundCount}); - - return SingleChildScrollView( - controller: controller, - physics: const ClampingScrollPhysics(), - child: Card( - margin: EdgeInsets.zero, - shape: context.isMobile - ? const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topRight: Radius.circular(20), topLeft: Radius.circular(20)), - ) - : const BeveledRectangleBorder(), - elevation: 0.0, - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 15), - const CustomDraggingHandle(), - const SizedBox(height: 15), - Center( - child: Text( - assetsInBoundsText, - style: TextStyle( - fontSize: 20, - color: context.textTheme.displayLarge?.color?.withValues(alpha: 0.75), - fontWeight: FontWeight.w500, - ), - ), - ), - const SizedBox(height: 8), - ], - ), - ValueListenableBuilder( - valueListenable: assetInSheet, - builder: (_, value, __) => Visibility( - visible: value != null, - child: Positioned( - right: 18, - top: 24, - child: IconButton( - icon: Icon(Icons.map_outlined, color: context.textTheme.displayLarge?.color), - iconSize: 24, - tooltip: 'zoom_to_bounds'.tr(), - onPressed: () => onZoomToAsset?.call(value!), - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/map/map_bottom_sheet.dart b/mobile/lib/widgets/map/map_bottom_sheet.dart deleted file mode 100644 index fba9e9a041..0000000000 --- a/mobile/lib/widgets/map/map_bottom_sheet.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; -import 'package:immich_mobile/utils/draggable_scroll_controller.dart'; -import 'package:immich_mobile/widgets/map/map_asset_grid.dart'; - -class MapBottomSheet extends HookConsumerWidget { - final Stream mapEventStream; - final Function(String)? onGridAssetChanged; - final Function(String)? onZoomToAsset; - final Function()? onZoomToLocation; - final Function(bool, Set)? onAssetsSelected; - final ValueNotifier> selectedAssets; - - const MapBottomSheet({ - required this.mapEventStream, - this.onGridAssetChanged, - this.onZoomToAsset, - this.onAssetsSelected, - this.onZoomToLocation, - required this.selectedAssets, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - const sheetMinExtent = 0.1; - final sheetController = useDraggableScrollController(); - final bottomSheetOffset = useValueNotifier(sheetMinExtent); - final isBottomSheetOpened = useRef(false); - - void handleMapEvents(MapEvent event) async { - if (event is MapCloseBottomSheet) { - await sheetController.animateTo( - 0.1, - duration: const Duration(milliseconds: 200), - curve: Curves.linearToEaseOut, - ); - } - } - - useOnStreamChange(mapEventStream, onData: handleMapEvents); - - bool onScrollNotification(DraggableScrollableNotification notification) { - isBottomSheetOpened.value = notification.extent > (notification.maxExtent * 0.9); - bottomSheetOffset.value = notification.extent; - // do not bubble - return true; - } - - return Stack( - children: [ - NotificationListener( - onNotification: onScrollNotification, - child: DraggableScrollableSheet( - controller: sheetController, - minChildSize: sheetMinExtent, - maxChildSize: 0.8, - initialChildSize: sheetMinExtent, - snap: true, - snapSizes: [sheetMinExtent, 0.5, 0.8], - shouldCloseOnMinExtent: false, - builder: (ctx, scrollController) => MapAssetGrid( - controller: scrollController, - mapEventStream: mapEventStream, - selectedAssets: selectedAssets, - onAssetsSelected: onAssetsSelected, - // Do not bother with the event if the bottom sheet is not user scrolled - onGridAssetChanged: (assetId) => isBottomSheetOpened.value ? onGridAssetChanged?.call(assetId) : null, - onZoomToAsset: onZoomToAsset, - ), - ), - ), - ValueListenableBuilder( - valueListenable: bottomSheetOffset, - builder: (context, value, child) { - return Positioned( - right: 0, - bottom: context.height * (value + 0.02), - child: AnimatedOpacity( - opacity: value < 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/widgets/map/map_settings_sheet.dart b/mobile/lib/widgets/map/map_settings_sheet.dart deleted file mode 100644 index 644056d153..0000000000 --- a/mobile/lib/widgets/map/map_settings_sheet.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/widgets/map/map_settings/map_settings_list_tile.dart'; -import 'package:immich_mobile/widgets/map/map_settings/map_settings_time_dropdown.dart'; -import 'package:immich_mobile/widgets/map/map_settings/map_theme_picker.dart'; - -class MapSettingsSheet extends HookConsumerWidget { - const MapSettingsSheet({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mapState = ref.watch(mapStateNotifierProvider); - - return DraggableScrollableSheet( - expand: false, - initialChildSize: 0.6, - builder: (ctx, scrollController) => SingleChildScrollView( - controller: scrollController, - child: Card( - elevation: 0.0, - shadowColor: Colors.transparent, - margin: EdgeInsets.zero, - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - MapThemePicker( - themeMode: mapState.themeMode, - onThemeChange: (mode) => ref.read(mapStateNotifierProvider.notifier).switchTheme(mode), - ), - const Divider(height: 30, thickness: 2), - MapSettingsListTile( - title: "map_settings_only_show_favorites", - selected: mapState.showFavoriteOnly, - onChanged: (favoriteOnly) => - ref.read(mapStateNotifierProvider.notifier).switchFavoriteOnly(favoriteOnly), - ), - MapSettingsListTile( - title: "map_settings_include_show_archived", - selected: mapState.includeArchived, - onChanged: (includeArchive) => - ref.read(mapStateNotifierProvider.notifier).switchIncludeArchived(includeArchive), - ), - MapSettingsListTile( - title: "map_settings_include_show_partners", - selected: mapState.withPartners, - onChanged: (withPartners) => - ref.read(mapStateNotifierProvider.notifier).switchWithPartners(withPartners), - ), - MapTimeDropDown( - relativeTime: mapState.relativeTime, - onTimeChange: (time) => ref.read(mapStateNotifierProvider.notifier).setRelativeTime(time), - ), - const SizedBox(height: 20), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart b/mobile/lib/widgets/map/positioned_asset_marker_icon.dart deleted file mode 100644 index b6d7241cf4..0000000000 --- a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:io'; -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/map/asset_marker_icon.dart'; - -class PositionedAssetMarkerIcon extends StatelessWidget { - final Point point; - final String assetRemoteId; - final String assetThumbhash; - final double size; - final int durationInMilliseconds; - - final Function()? onTap; - - const PositionedAssetMarkerIcon({ - required this.point, - required this.assetRemoteId, - required this.assetThumbhash, - this.size = 100, - this.durationInMilliseconds = 100, - this.onTap, - super.key, - }); - - @override - Widget build(BuildContext context) { - final ratio = Platform.isIOS ? 1.0 : context.devicePixelRatio; - return AnimatedPositioned( - left: point.x / ratio - size / 2, - top: point.y / ratio - size, - duration: Duration(milliseconds: durationInMilliseconds), - child: GestureDetector( - onTap: () => onTap?.call(), - child: SizedBox.square( - dimension: size, - child: AssetMarkerIcon(id: assetRemoteId, thumbhash: assetThumbhash, key: Key(assetRemoteId)), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/memories/memory_bottom_info.dart b/mobile/lib/widgets/memories/memory_bottom_info.dart deleted file mode 100644 index 4b43821782..0000000000 --- a/mobile/lib/widgets/memories/memory_bottom_info.dart +++ /dev/null @@ -1,50 +0,0 @@ -// ignore_for_file: require_trailing_commas - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; - -class MemoryBottomInfo extends StatelessWidget { - final Memory memory; - - const MemoryBottomInfo({super.key, required this.memory}); - - @override - Widget build(BuildContext context) { - final df = DateFormat.yMMMMd(); - return Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - memory.title, - style: TextStyle(color: Colors.grey[400], fontSize: 13.0, fontWeight: FontWeight.w500), - ), - Text( - df.format(memory.assets[0].fileCreatedAt), - style: const TextStyle(color: Colors.white, fontSize: 15.0, fontWeight: FontWeight.w500), - ), - ], - ), - MaterialButton( - minWidth: 0, - onPressed: () { - context.maybePop(); - scrollToDateNotifierProvider.scrollToDate(memory.assets[0].fileCreatedAt); - }, - shape: const CircleBorder(), - color: Colors.white.withValues(alpha: 0.2), - elevation: 0, - child: const Icon(Icons.open_in_new, color: Colors.white), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/memories/memory_card.dart b/mobile/lib/widgets/memories/memory_card.dart deleted file mode 100644 index 189cc67428..0000000000 --- a/mobile/lib/widgets/memories/memory_card.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; -import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; - -class MemoryCard extends StatelessWidget { - final Asset asset; - final String title; - final bool showTitle; - final Function()? onVideoEnded; - - const MemoryCard({required this.asset, required this.title, required this.showTitle, this.onVideoEnded, super.key}); - - @override - Widget build(BuildContext context) { - return Card( - color: Colors.black, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(25.0)), - side: BorderSide(color: Colors.black, width: 1.0), - ), - clipBehavior: Clip.hardEdge, - child: Stack( - children: [ - SizedBox.expand(child: _BlurredBackdrop(asset: asset)), - LayoutBuilder( - builder: (context, constraints) { - // Determine the fit using the aspect ratio - BoxFit fit = BoxFit.contain; - if (asset.width != null && asset.height != null) { - final aspectRatio = asset.width! / asset.height!; - final phoneAspectRatio = constraints.maxWidth / constraints.maxHeight; - // Look for a 25% difference in either direction - if (phoneAspectRatio * .75 < aspectRatio && phoneAspectRatio * 1.25 > aspectRatio) { - // Cover to look nice if we have nearly the same aspect ratio - fit = BoxFit.cover; - } - } - - if (asset.isImage) { - return Hero( - tag: 'memory-${asset.id}', - child: ImmichImage(asset, fit: fit, height: double.infinity, width: double.infinity), - ); - } else { - return Hero( - tag: 'memory-${asset.id}', - child: SizedBox( - width: context.width, - height: context.height, - child: NativeVideoViewerPage( - key: ValueKey(asset.id), - asset: asset, - showControls: false, - playbackDelayFactor: 2, - image: ImmichImage(asset, width: context.width, height: context.height, fit: BoxFit.contain), - ), - ), - ); - } - }, - ), - if (showTitle) - Positioned( - left: 18.0, - bottom: 18.0, - child: Text( - title, - style: context.textTheme.headlineMedium?.copyWith(color: Colors.white, fontWeight: FontWeight.w500), - ), - ), - ], - ), - ); - } -} - -class _BlurredBackdrop extends HookWidget { - final Asset asset; - - const _BlurredBackdrop({required this.asset}); - - @override - Widget build(BuildContext context) { - final blurhash = useBlurHashRef(asset).value; - if (blurhash != null) { - // Use a nice cheap blur hash image decoration - return Container( - decoration: BoxDecoration( - image: DecorationImage(image: MemoryImage(blurhash), fit: BoxFit.cover), - ), - child: Container(color: Colors.black.withValues(alpha: 0.2)), - ); - } else { - // Fall back to using a more expensive image filtered - // Since the ImmichImage is already precached, we can - // safely use that as the image provider - return ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), - child: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: ImmichImage.imageProvider(asset: asset, height: context.height, width: context.width), - fit: BoxFit.cover, - ), - ), - child: Container(color: Colors.black.withValues(alpha: 0.2)), - ), - ); - } - } -} diff --git a/mobile/lib/widgets/memories/memory_lane.dart b/mobile/lib/widgets/memories/memory_lane.dart deleted file mode 100644 index 4cba83bea7..0000000000 --- a/mobile/lib/widgets/memories/memory_lane.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; - -class MemoryLane extends HookConsumerWidget { - const MemoryLane({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final memoryLaneFutureProvider = ref.watch(memoryFutureProvider); - - final memoryLane = memoryLaneFutureProvider - .whenData( - (memories) => memories != null - ? ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200), - child: CarouselView( - itemExtent: 145.0, - shrinkExtent: 1.0, - elevation: 2, - backgroundColor: Colors.black, - overlayColor: WidgetStateProperty.all(Colors.white.withValues(alpha: 0.1)), - onTap: (memoryIndex) { - ref.read(hapticFeedbackProvider.notifier).heavyImpact(); - if (memories[memoryIndex].assets.isNotEmpty) { - final asset = memories[memoryIndex].assets[0]; - ref.read(currentAssetProvider.notifier).set(asset); - } - context.pushRoute(MemoryRoute(memories: memories, memoryIndex: memoryIndex)); - }, - children: memories - .mapIndexed((index, memory) => MemoryCard(index: index, memory: memory)) - .toList(), - ), - ) - : const SizedBox(), - ) - .value; - - return memoryLane ?? const SizedBox(); - } -} - -class MemoryCard extends ConsumerWidget { - const MemoryCard({super.key, required this.index, required this.memory}); - - final int index; - final Memory memory; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Center( - child: Stack( - children: [ - ColorFiltered( - colorFilter: ColorFilter.mode(Colors.black.withValues(alpha: 0.2), BlendMode.darken), - child: Hero( - tag: 'memory-${memory.assets[0].id}', - child: ImmichImage( - memory.assets[0], - fit: BoxFit.cover, - width: 205, - height: 200, - placeholder: const ThumbnailPlaceholder(width: 105, height: 200), - ), - ), - ), - Positioned( - bottom: 16, - left: 16, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 114), - child: Text( - memory.title, - style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 15), - ), - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/search/curated_people_row.dart b/mobile/lib/widgets/search/curated_people_row.dart deleted file mode 100644 index 9155de2131..0000000000 --- a/mobile/lib/widgets/search/curated_people_row.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; - -class CuratedPeopleRow extends StatelessWidget { - static const double imageSize = 60.0; - - final List content; - final EdgeInsets? padding; - - /// Callback with the content and the index when tapped - final Function(SearchCuratedContent, int)? onTap; - final Function(SearchCuratedContent, int)? onNameTap; - - const CuratedPeopleRow({super.key, required this.content, this.onTap, this.padding, required this.onNameTap}); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - child: SingleChildScrollView( - padding: padding, - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: List.generate(content.length, (index) { - final person = content[index]; - return Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - GestureDetector( - onTap: () => onTap?.call(person, index), - child: SizedBox( - height: imageSize, - child: Material( - shape: const CircleBorder(side: BorderSide.none), - elevation: 3, - child: CircleAvatar( - maxRadius: imageSize / 2, - backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), - ), - ), - ), - ), - const SizedBox(height: 8), - SizedBox(width: imageSize, child: _buildPersonLabel(context, person, index)), - ], - ), - ); - }), - ), - ), - ); - } - - Widget _buildPersonLabel(BuildContext context, SearchCuratedContent person, int index) { - if (person.label.isEmpty) { - return GestureDetector( - onTap: () => onNameTap?.call(person, index), - child: Text( - "exif_bottom_sheet_person_add_person", - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - ).tr(), - ); - } - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - person.label, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: context.textTheme.labelLarge, - maxLines: 2, - ), - if (person.subtitle != null) Text(person.subtitle!, textAlign: TextAlign.center), - ], - ); - } -} diff --git a/mobile/lib/widgets/search/curated_places_row.dart b/mobile/lib/widgets/search/curated_places_row.dart deleted file mode 100644 index 9d21292bde..0000000000 --- a/mobile/lib/widgets/search/curated_places_row.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/widgets/search/search_map_thumbnail.dart'; -import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart'; - -class CuratedPlacesRow extends StatelessWidget { - const CuratedPlacesRow({ - super.key, - required this.content, - required this.imageSize, - this.isMapEnabled = true, - this.onTap, - }); - - final bool isMapEnabled; - final List content; - final double imageSize; - - /// Callback with the content and the index when tapped - final Function(SearchCuratedContent, int)? onTap; - - @override - Widget build(BuildContext context) { - // Calculating the actual index of the content based on the whether map is enabled or not. - // If enabled, inject map as the first item in the list (index 0) and so the actual content will start from index 1 - final int actualContentIndex = isMapEnabled ? 1 : 0; - - return SizedBox( - height: imageSize, - child: ListView.separated( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16), - separatorBuilder: (context, index) => const SizedBox(width: 10), - itemBuilder: (context, index) { - // Injecting Map thumbnail as the first element - if (isMapEnabled && index == 0) { - return SizedBox.square( - dimension: imageSize, - child: SearchMapThumbnail(size: imageSize), - ); - } - final actualIndex = index - actualContentIndex; - final object = content[actualIndex]; - final thumbnailRequestUrl = '${Store.get(StoreKey.serverEndpoint)}/assets/${object.id}/thumbnail'; - return SizedBox.square( - dimension: imageSize, - child: ThumbnailWithInfo( - imageUrl: thumbnailRequestUrl, - textInfo: object.label, - onTap: () => onTap?.call(object, actualIndex), - ), - ); - }, - itemCount: content.length + actualContentIndex, - ), - ); - } -} diff --git a/mobile/lib/widgets/search/explore_grid.dart b/mobile/lib/widgets/search/explore_grid.dart deleted file mode 100644 index 6af20df029..0000000000 --- a/mobile/lib/widgets/search/explore_grid.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart'; - -class ExploreGrid extends StatelessWidget { - final List curatedContent; - final bool isPeople; - - const ExploreGrid({super.key, required this.curatedContent, this.isPeople = false}); - - @override - Widget build(BuildContext context) { - if (curatedContent.isEmpty) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: SizedBox( - height: 100, - width: 100, - child: ThumbnailWithInfo(textInfo: '', onTap: () {}), - ), - ); - } - - return GridView.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 140, - mainAxisSpacing: 4, - crossAxisSpacing: 4, - ), - itemBuilder: (context, index) { - final content = curatedContent[index]; - final thumbnailRequestUrl = isPeople - ? getFaceThumbnailUrl(content.id) - : '${Store.get(StoreKey.serverEndpoint)}/assets/${content.id}/thumbnail'; - - return ThumbnailWithInfo( - imageUrl: thumbnailRequestUrl, - textInfo: content.label, - borderRadius: 0, - onTap: () { - isPeople - ? context.pushRoute(PersonResultRoute(personId: content.id, personName: content.label)) - : context.pushRoute( - SearchRoute( - prefilter: SearchFilter( - people: {}, - location: SearchLocationFilter(city: content.label), - camera: SearchCameraFilter(), - date: SearchDateFilter(), - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - rating: SearchRatingFilter(), - mediaType: AssetType.other, - ), - ), - ); - }, - ); - }, - itemCount: curatedContent.length, - ); - } -} diff --git a/mobile/lib/widgets/search/person_name_edit_form.dart b/mobile/lib/widgets/search/person_name_edit_form.dart deleted file mode 100644 index 3fa443121a..0000000000 --- a/mobile/lib/widgets/search/person_name_edit_form.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; - -class PersonNameEditFormResult { - final bool success; - final String updatedName; - - const PersonNameEditFormResult(this.success, this.updatedName); -} - -class PersonNameEditForm extends HookConsumerWidget { - final String personId; - final String personName; - - const PersonNameEditForm({super.key, required this.personId, required this.personName}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final controller = useTextEditingController(text: personName); - final isError = useState(false); - - return AlertDialog( - title: const Text("add_a_name", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - content: SingleChildScrollView( - child: TextFormField( - controller: controller, - textCapitalization: TextCapitalization.words, - autofocus: true, - decoration: InputDecoration( - hintText: 'name'.tr(), - border: const OutlineInputBorder(), - errorText: isError.value ? 'Error occurred' : null, - ), - ), - ), - actions: [ - TextButton( - onPressed: () => context.pop(const PersonNameEditFormResult(false, '')), - child: Text( - "cancel", - style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold), - ).tr(), - ), - TextButton( - onPressed: () async { - isError.value = false; - final result = await ref.read(updatePersonNameProvider(personId, controller.text).future); - isError.value = !result; - if (result) { - context.pop(PersonNameEditFormResult(true, controller.text)); - } - }, - child: Text( - "save", - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/search/search_filter/camera_picker.dart b/mobile/lib/widgets/search/search_filter/camera_picker.dart index a5204c2fbc..6a025bdb69 100644 --- a/mobile/lib/widgets/search/search_filter/camera_picker.dart +++ b/mobile/lib/widgets/search/search_filter/camera_picker.dart @@ -21,9 +21,13 @@ class CameraPicker extends HookConsumerWidget { final selectedMake = useState(filter?.make); final selectedModel = useState(filter?.model); - final make = ref.watch(getSearchSuggestionsProvider(SearchSuggestionType.cameraMake)); + final make = ref.watch(getSearchSuggestionsProvider(SearchSuggestionArgs(type: SearchSuggestionType.cameraMake))); - final models = ref.watch(getSearchSuggestionsProvider(SearchSuggestionType.cameraModel, make: selectedMake.value)); + final models = ref.watch( + getSearchSuggestionsProvider( + SearchSuggestionArgs(type: SearchSuggestionType.cameraModel, make: selectedMake.value), + ), + ); final makeWidget = SearchDropdown( dropdownMenuEntries: switch (make) { diff --git a/mobile/lib/widgets/search/search_filter/common/dropdown.dart b/mobile/lib/widgets/search/search_filter/common/dropdown.dart index 70cbfd2c15..9d02f4f3e8 100644 --- a/mobile/lib/widgets/search/search_filter/common/dropdown.dart +++ b/mobile/lib/widgets/search/search_filter/common/dropdown.dart @@ -16,9 +16,15 @@ class SearchDropdown extends StatelessWidget { final Widget? label; final Widget? leadingIcon; + static const WidgetStatePropertyAll _optionPadding = WidgetStatePropertyAll( + EdgeInsetsDirectional.fromSTEB(16, 0, 16, 0), + ); + @override Widget build(BuildContext context) { - final menuStyle = const MenuStyle( + final mediaQuery = MediaQuery.of(context); + final maxMenuHeight = mediaQuery.size.height * 0.5 - mediaQuery.viewPadding.bottom; + const menuStyle = MenuStyle( shape: WidgetStatePropertyAll( RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15))), ), @@ -26,11 +32,26 @@ class SearchDropdown extends StatelessWidget { return LayoutBuilder( builder: (context, constraints) { + final styledEntries = dropdownMenuEntries + .map( + (entry) => DropdownMenuEntry( + value: entry.value, + label: entry.label, + labelWidget: entry.labelWidget, + enabled: entry.enabled, + leadingIcon: entry.leadingIcon, + trailingIcon: entry.trailingIcon, + style: (entry.style ?? const ButtonStyle()).copyWith(padding: _optionPadding), + ), + ) + .toList(growable: false); + return DropdownMenu( controller: controller, leadingIcon: leadingIcon, width: constraints.maxWidth, - dropdownMenuEntries: dropdownMenuEntries, + menuHeight: maxMenuHeight, + dropdownMenuEntries: styledEntries, label: label, menuStyle: menuStyle, trailingIcon: const Icon(Icons.arrow_drop_down_rounded), diff --git a/mobile/lib/widgets/search/search_filter/location_picker.dart b/mobile/lib/widgets/search/search_filter/location_picker.dart index 608183a2f6..f521a50f35 100644 --- a/mobile/lib/widgets/search/search_filter/location_picker.dart +++ b/mobile/lib/widgets/search/search_filter/location_picker.dart @@ -25,25 +25,31 @@ class LocationPicker extends HookConsumerWidget { final countries = ref.watch( getSearchSuggestionsProvider( - SearchSuggestionType.country, - locationCountry: selectedCountry.value, - locationState: selectedState.value, + SearchSuggestionArgs( + type: SearchSuggestionType.country, + locationCountry: selectedCountry.value, + locationState: selectedState.value, + ), ), ); final states = ref.watch( getSearchSuggestionsProvider( - SearchSuggestionType.state, - locationCountry: selectedCountry.value, - locationState: selectedState.value, + SearchSuggestionArgs( + type: SearchSuggestionType.state, + locationCountry: selectedCountry.value, + locationState: selectedState.value, + ), ), ); final cities = ref.watch( getSearchSuggestionsProvider( - SearchSuggestionType.city, - locationCountry: selectedCountry.value, - locationState: selectedState.value, + SearchSuggestionArgs( + type: SearchSuggestionType.city, + locationCountry: selectedCountry.value, + locationState: selectedState.value, + ), ), ); 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 e0e34b654e..ac89de8190 100644 --- a/mobile/lib/widgets/search/search_filter/media_type_picker.dart +++ b/mobile/lib/widgets/search/search_filter/media_type_picker.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; class MediaTypePicker extends HookWidget { const MediaTypePicker({super.key, required this.onSelect, this.filter}); diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index 978b70239c..a7b0286df3 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -57,6 +57,7 @@ class PeoplePicker extends HookConsumerWidget { final isSelected = selectedPeople.value.contains(person); return Padding( + key: ValueKey(person.id), padding: const EdgeInsets.only(bottom: 2.0), child: LargeLeadingTile( title: Text( @@ -73,6 +74,7 @@ class PeoplePicker extends HookConsumerWidget { shape: const CircleBorder(side: BorderSide.none), elevation: 3, child: CircleAvatar( + key: ValueKey(person.id), maxRadius: imageSize / 2, backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), ), diff --git a/mobile/lib/widgets/search/search_filter/search_filter_chip.dart b/mobile/lib/widgets/search/search_filter/search_filter_chip.dart index a72b4668dd..d539ee38f3 100644 --- a/mobile/lib/widgets/search/search_filter/search_filter_chip.dart +++ b/mobile/lib/widgets/search/search_filter/search_filter_chip.dart @@ -16,7 +16,7 @@ class SearchFilterChip extends StatelessWidget { onTap: onTap, child: Card( elevation: 0, - color: context.primaryColor.withValues(alpha: .5), + color: context.colorScheme.secondaryContainer, shape: StadiumBorder(side: BorderSide(color: context.colorScheme.secondaryContainer)), child: Padding( padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0), @@ -32,7 +32,13 @@ class SearchFilterChip extends StatelessWidget { shape: StadiumBorder(side: BorderSide(color: context.colorScheme.outline.withAlpha(15))), child: Padding( padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0), - child: Row(children: [Icon(icon, size: 18), const SizedBox(width: 4.0), Text(label)]), + child: Row( + children: [ + Icon(icon, size: 18), + const SizedBox(width: 4.0), + Text(label, style: TextStyle(color: context.colorScheme.onSecondaryContainer)), + ], + ), ), ), ); diff --git a/mobile/lib/widgets/search/search_map_thumbnail.dart b/mobile/lib/widgets/search/search_map_thumbnail.dart deleted file mode 100644 index 7533e46f1a..0000000000 --- a/mobile/lib/widgets/search/search_map_thumbnail.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:immich_mobile/widgets/search/thumbnail_with_info_container.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -class SearchMapThumbnail extends StatelessWidget { - const SearchMapThumbnail({super.key, this.size = 60.0}); - - final double size; - final bool showTitle = true; - - @override - Widget build(BuildContext context) { - return ThumbnailWithInfoContainer( - label: 'search_page_your_map'.tr(), - onTap: () { - context.pushRoute(MapRoute()); - }, - child: IgnorePointer( - child: MapThumbnail(zoom: 2, centre: const LatLng(47, 5), height: size, width: size, showAttribution: false), - ), - ); - } -} diff --git a/mobile/lib/widgets/search/search_row_section.dart b/mobile/lib/widgets/search/search_row_section.dart deleted file mode 100644 index b8584fefef..0000000000 --- a/mobile/lib/widgets/search/search_row_section.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/search/search_row_title.dart'; - -class SearchRowSection extends StatelessWidget { - const SearchRowSection({ - super.key, - required this.onViewAllPressed, - required this.title, - this.isEmpty = false, - required this.child, - }); - - final Function() onViewAllPressed; - final String title; - final bool isEmpty; - final Widget child; - - @override - Widget build(BuildContext context) { - if (isEmpty) { - return const SizedBox.shrink(); - } - - return Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: SearchRowTitle(onViewAllPressed: onViewAllPressed, title: title), - ), - child, - ], - ); - } -} diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index d5905a246c..a38ccd3556 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; @@ -14,9 +13,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/bytes_units.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart'; import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart'; -import 'package:immich_mobile/widgets/settings/local_storage_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_action_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; @@ -35,7 +32,6 @@ class AdvancedSettings extends HookConsumerWidget { final manageMediaAndroidPermission = useState(false); final levelId = useAppSettingsState(AppSettingsEnum.logLevel); final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage); - final useAlternatePMFilter = useAppSettingsState(AppSettingsEnum.photoManagerCustomFilter); final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled); final logLevel = Level.LEVELS[levelId.value].name; @@ -114,35 +110,26 @@ class AdvancedSettings extends HookConsumerWidget { title: "advanced_settings_prefer_remote_title".tr(), subtitle: "advanced_settings_prefer_remote_subtitle".tr(), ), - if (!Store.isBetaTimelineEnabled) const LocalStorageSettings(), const CustomProxyHeaderSettings(), const SslClientCertSettings(), - if (!Store.isBetaTimelineEnabled) - SettingsSwitchListTile( - valueNotifier: useAlternatePMFilter, - title: "advanced_settings_enable_alternate_media_filter_title".tr(), - subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(), - ), - if (!Store.isBetaTimelineEnabled) const BetaTimelineListTile(), - if (Store.isBetaTimelineEnabled) - SettingsSwitchListTile( - valueNotifier: readonlyModeEnabled, - title: "advanced_settings_readonly_mode_title".tr(), - subtitle: "advanced_settings_readonly_mode_subtitle".tr(), - onChanged: (value) { - readonlyModeEnabled.value = value; - ref.read(readonlyModeProvider.notifier).setReadonlyMode(value); - context.scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 2), - content: Text( - (value ? "readonly_mode_enabled" : "readonly_mode_disabled").tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), + SettingsSwitchListTile( + valueNotifier: readonlyModeEnabled, + title: "advanced_settings_readonly_mode_title".tr(), + subtitle: "advanced_settings_readonly_mode_subtitle".tr(), + onChanged: (value) { + readonlyModeEnabled.value = value; + ref.read(readonlyModeProvider.notifier).setReadonlyMode(value); + context.scaffoldMessenger.showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), + content: Text( + (value ? "readonly_mode_enabled" : "readonly_mode_disabled").tr(), + style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), - ); - }, - ), + ), + ); + }, + ), ListTile( title: Text("advanced_settings_clear_image_cache".tr(), style: const TextStyle(fontWeight: FontWeight.w500)), leading: const Icon(Icons.playlist_remove_rounded), diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart index 08e66df48d..42ea3acfc0 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart'; diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart index 2d5c9f06eb..55c8195947 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart @@ -1,21 +1,18 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; class LayoutSettings extends HookConsumerWidget { const LayoutSettings({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final useDynamicLayout = useAppSettingsState(AppSettingsEnum.dynamicLayout); final tilesPerRow = useAppSettingsState(AppSettingsEnum.tilesPerRow); return Column( @@ -25,12 +22,6 @@ class LayoutSettings extends HookConsumerWidget { title: "asset_list_layout_sub_title".t(context: context), icon: Icons.view_module_outlined, ), - if (!Store.isBetaTimelineEnabled) - SettingsSwitchListTile( - valueNotifier: useDynamicLayout, - title: "asset_list_layout_settings_dynamic_layout_title".t(context: context), - onChanged: (_) => ref.invalidate(appSettingsServiceProvider), - ), SettingsSliderListTile( valueNotifier: tilesPerRow, text: 'theme_setting_asset_list_tiles_per_row_title'.tr(namedArgs: {'count': "${tilesPerRow.value}"}), diff --git a/mobile/lib/widgets/settings/backup_settings/background_settings.dart b/mobile/lib/widgets/settings/backup_settings/background_settings.dart deleted file mode 100644 index 038a567dc2..0000000000 --- a/mobile/lib/widgets/settings/backup_settings/background_settings.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/widgets/backup/ios_debug_info_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class BackgroundBackupSettings extends ConsumerWidget { - const BackgroundBackupSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isBackgroundEnabled = ref.watch(backupProvider.select((s) => s.backgroundBackup)); - final iosSettings = ref.watch(iOSBackgroundSettingsProvider); - - void showErrorToUser(String msg) { - final snackBar = SnackBar( - content: Text(msg.tr(), style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor)), - backgroundColor: Colors.red, - ); - context.scaffoldMessenger.showSnackBar(snackBar); - } - - void showBatteryOptimizationInfoToUser() { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext ctx) { - return AlertDialog( - title: const Text('backup_controller_page_background_battery_info_title').tr(), - content: SingleChildScrollView( - child: const Text('backup_controller_page_background_battery_info_message').tr(), - ), - actions: [ - ElevatedButton( - onPressed: () => - launchUrl(Uri.parse('https://dontkillmyapp.com'), mode: LaunchMode.externalApplication), - child: const Text( - "backup_controller_page_background_battery_info_link", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - ).tr(), - ), - ElevatedButton( - child: const Text( - 'backup_controller_page_background_battery_info_ok', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - ).tr(), - onPressed: () => ctx.pop(), - ), - ], - ); - }, - ); - } - - if (!isBackgroundEnabled) { - return SettingsButtonListTile( - icon: Icons.cloud_sync_outlined, - title: 'backup_controller_page_background_is_off'.tr(), - subtileText: 'backup_controller_page_background_description'.tr(), - buttonText: 'backup_controller_page_background_turn_on'.tr(), - onButtonTap: () => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - enabled: true, - onError: showErrorToUser, - onBatteryInfo: showBatteryOptimizationInfoToUser, - ), - ); - } - - return Column( - children: [ - if (!Platform.isIOS || iosSettings?.appRefreshEnabled == true) - _BackgroundSettingsEnabled(onError: showErrorToUser, onBatteryInfo: showBatteryOptimizationInfoToUser), - if (Platform.isIOS && iosSettings?.appRefreshEnabled != true) const _IOSBackgroundRefreshDisabled(), - if (Platform.isIOS && iosSettings != null) IosDebugInfoTile(settings: iosSettings), - ], - ); - } -} - -class _IOSBackgroundRefreshDisabled extends StatelessWidget { - const _IOSBackgroundRefreshDisabled(); - - @override - Widget build(BuildContext context) { - return SettingsButtonListTile( - icon: Icons.task_outlined, - title: 'backup_controller_page_background_app_refresh_disabled_title'.tr(), - subtileText: 'backup_controller_page_background_app_refresh_disabled_content'.tr(), - buttonText: 'backup_controller_page_background_app_refresh_enable_button_text'.tr(), - onButtonTap: () => openAppSettings(), - ); - } -} - -class _BackgroundSettingsEnabled extends HookConsumerWidget { - final void Function(String msg) onError; - final void Function() onBatteryInfo; - - const _BackgroundSettingsEnabled({required this.onError, required this.onBatteryInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isWifiRequired = ref.watch(backupProvider.select((s) => s.backupRequireWifi)); - final isWifiRequiredNotifier = useValueNotifier(isWifiRequired); - useValueChanged( - isWifiRequired, - (_, __) => WidgetsBinding.instance.addPostFrameCallback((_) => isWifiRequiredNotifier.value = isWifiRequired), - ); - - final isChargingRequired = ref.watch(backupProvider.select((s) => s.backupRequireCharging)); - final isChargingRequiredNotifier = useValueNotifier(isChargingRequired); - useValueChanged( - isChargingRequired, - (_, __) => - WidgetsBinding.instance.addPostFrameCallback((_) => isChargingRequiredNotifier.value = isChargingRequired), - ); - - int backupDelayToSliderValue(int ms) => switch (ms) { - 5000 => 0, - 30000 => 1, - 120000 => 2, - _ => 3, - }; - - int backupDelayToMilliseconds(int v) => switch (v) { - 0 => 5000, - 1 => 30000, - 2 => 120000, - _ => 600000, - }; - - 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'}), - }; - - final backupTriggerDelay = ref.watch(backupProvider.select((s) => s.backupTriggerDelay)); - final triggerDelay = useState(backupDelayToSliderValue(backupTriggerDelay)); - useValueChanged( - triggerDelay.value, - (_, __) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - triggerDelay: backupDelayToMilliseconds(triggerDelay.value), - onError: onError, - onBatteryInfo: onBatteryInfo, - ), - ); - - return SettingsButtonListTile( - icon: Icons.cloud_sync_rounded, - iconColor: context.primaryColor, - title: 'backup_controller_page_background_is_on'.tr(), - buttonText: 'backup_controller_page_background_turn_off'.tr(), - onButtonTap: () => ref - .read(backupProvider.notifier) - .configureBackgroundBackup(enabled: false, onError: onError, onBatteryInfo: onBatteryInfo), - subtitle: Column( - children: [ - SettingsSwitchListTile( - valueNotifier: isWifiRequiredNotifier, - title: 'backup_controller_page_background_wifi'.tr(), - icon: Icons.wifi, - onChanged: (enabled) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup(requireWifi: enabled, onError: onError, onBatteryInfo: onBatteryInfo), - ), - SettingsSwitchListTile( - valueNotifier: isChargingRequiredNotifier, - title: 'backup_controller_page_background_charging'.tr(), - icon: Icons.charging_station, - onChanged: (enabled) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup(requireCharging: enabled, onError: onError, onBatteryInfo: onBatteryInfo), - ), - if (Platform.isAndroid) - SettingsSliderListTile( - valueNotifier: triggerDelay, - text: 'backup_controller_page_background_delay'.tr( - namedArgs: {'duration': formatBackupDelaySliderValue(triggerDelay.value)}, - ), - maxValue: 3.0, - noDivisons: 3, - label: formatBackupDelaySliderValue(triggerDelay.value), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart deleted file mode 100644 index 50aa57da9f..0000000000 --- a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/backup/backup_verification.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/background_settings.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/foreground_settings.dart'; -import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; - -class BackupSettings extends HookConsumerWidget { - const BackupSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ignoreIcloudAssets = useAppSettingsState(AppSettingsEnum.ignoreIcloudAssets); - final isAdvancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); - final albumSync = useAppSettingsState(AppSettingsEnum.syncAlbums); - final isCorruptCheckInProgress = ref.watch(backupVerificationProvider); - final isAlbumSyncInProgress = useState(false); - - syncAlbums() async { - isAlbumSyncInProgress.value = true; - try { - await ref.read(assetServiceProvider).syncUploadedAssetToAlbums(); - } catch (_) { - } finally { - Future.delayed(const Duration(seconds: 1), () { - isAlbumSyncInProgress.value = false; - }); - } - } - - final backupSettings = [ - const ForegroundBackupSettings(), - const BackgroundBackupSettings(), - if (Platform.isIOS) - SettingsSwitchListTile( - valueNotifier: ignoreIcloudAssets, - title: 'ignore_icloud_photos'.tr(), - subtitle: 'ignore_icloud_photos_description'.tr(), - ), - if (Platform.isAndroid && isAdvancedTroubleshooting.value) - SettingsButtonListTile( - icon: Icons.warning_rounded, - title: 'check_corrupt_asset_backup'.tr(), - subtitle: isCorruptCheckInProgress - ? const Column( - children: [ - SizedBox(height: 20), - Center(child: CircularProgressIndicator()), - SizedBox(height: 20), - ], - ) - : null, - subtileText: !isCorruptCheckInProgress ? 'check_corrupt_asset_backup_description'.tr() : null, - buttonText: 'check_corrupt_asset_backup_button'.tr(), - onButtonTap: !isCorruptCheckInProgress - ? () => ref.read(backupVerificationProvider.notifier).performBackupCheck(context) - : null, - ), - if (albumSync.value) - SettingsButtonListTile( - icon: Icons.photo_album_outlined, - title: 'sync_albums'.tr(), - subtitle: Text("sync_albums_manual_subtitle".tr()), - buttonText: 'sync_albums'.tr(), - child: isAlbumSyncInProgress.value - ? const CircularProgressIndicator() - : ElevatedButton(onPressed: syncAlbums, child: Text('sync'.tr())), - ), - ]; - - return SettingsSubPageScaffold(settings: backupSettings, showDivider: true); - } -} diff --git a/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart b/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart deleted file mode 100644 index a2ff00fe45..0000000000 --- a/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; - -class ForegroundBackupSettings extends ConsumerWidget { - const ForegroundBackupSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isAutoBackup = ref.watch(backupProvider.select((s) => s.autoBackup)); - - void onButtonTap() => ref.read(backupProvider.notifier).setAutoBackup(!isAutoBackup); - - if (isAutoBackup) { - return SettingsButtonListTile( - icon: Icons.cloud_done_rounded, - iconColor: context.primaryColor, - title: 'backup_controller_page_status_on'.tr(), - buttonText: 'backup_controller_page_turn_off'.tr(), - onButtonTap: onButtonTap, - ); - } - - return SettingsButtonListTile( - icon: Icons.cloud_off_rounded, - title: 'backup_controller_page_status_off'.tr(), - subtileText: 'backup_controller_page_desc_backup'.tr(), - buttonText: 'backup_controller_page_turn_on'.tr(), - onButtonTap: onButtonTap, - ); - } -} diff --git a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart deleted file mode 100644 index 21e0edb34c..0000000000 --- a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; - -class BetaTimelineListTile extends ConsumerWidget { - const BetaTimelineListTile({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final betaTimelineValue = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.betaTimeline); - final auth = ref.watch(authProvider); - - if (!auth.isAuthenticated) { - return const SizedBox.shrink(); - } - - 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(); - unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)])); - }, - child: Text("ok".t(context: context)), - ), - ], - ); - }, - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 4.0), - child: SettingListTile( - title: "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/free_up_space_settings.dart b/mobile/lib/widgets/settings/free_up_space_settings.dart index ee7ee20b00..01ee8426d0 100644 --- a/mobile/lib/widgets/settings/free_up_space_settings.dart +++ b/mobile/lib/widgets/settings/free_up_space_settings.dart @@ -703,11 +703,11 @@ class _DeleteConfirmationDialog extends StatelessWidget { ), actions: [ TextButton( - onPressed: () => context.pop(false), + onPressed: () => ContextHelper(context).pop(false), child: Text('cancel'.t(context: context)), ), ElevatedButton( - onPressed: () => context.pop(true), + onPressed: () => ContextHelper(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: context.colorScheme.error, foregroundColor: context.colorScheme.onError, @@ -747,7 +747,7 @@ class _DeleteSuccessDialog extends StatelessWidget { ), actions: [ ElevatedButton( - onPressed: () => context.pop(), + onPressed: () => ContextHelper(context).pop(), child: Text('done'.t(context: context)), ), ], diff --git a/mobile/lib/widgets/settings/local_storage_settings.dart b/mobile/lib/widgets/settings/local_storage_settings.dart deleted file mode 100644 index af9e4079bb..0000000000 --- a/mobile/lib/widgets/settings/local_storage_settings.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; - -class LocalStorageSettings extends HookConsumerWidget { - const LocalStorageSettings({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final isarDb = ref.watch(dbProvider); - final cacheItemCount = useState(0); - - useEffect(() { - cacheItemCount.value = isarDb.duplicatedAssets.countSync(); - return null; - }, []); - - void clearCache() async { - await isarDb.writeTxn(() => isarDb.duplicatedAssets.clear()); - cacheItemCount.value = await isarDb.duplicatedAssets.count(); - } - - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20), - dense: true, - title: Text( - "cache_settings_duplicated_assets_title", - style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), - ).tr(namedArgs: {'count': "${cacheItemCount.value}"}), - subtitle: Text( - "cache_settings_duplicated_assets_subtitle", - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), - trailing: TextButton( - onPressed: cacheItemCount.value > 0 ? clearCache : null, - child: Text( - "cache_settings_duplicated_assets_clear_button", - style: TextStyle( - fontSize: 12, - color: cacheItemCount.value > 0 ? Colors.red : Colors.grey, - fontWeight: FontWeight.bold, - ), - ).tr(), - ), - ); - } -} diff --git a/mobile/lib/wm_executor.dart b/mobile/lib/wm_executor.dart index 73e882e8e6..a10b651696 100644 --- a/mobile/lib/wm_executor.dart +++ b/mobile/lib/wm_executor.dart @@ -54,6 +54,9 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger { var _dynamicSpawning = false; var _isolatesCount = numberOfProcessors; + @visibleForTesting + UnmodifiableListView get pool => UnmodifiableListView(_pool); + @override Future init({int? isolatesCount, bool? dynamicSpawning}) async { if (_pool.isNotEmpty) { @@ -76,7 +79,9 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger { Future dispose() async { _queue.clear(); for (final worker in _pool) { - worker.kill(); + if (worker.initialized || worker.initializing) { + worker.kill(); + } } _pool.clear(); super.dispose(); @@ -157,9 +162,7 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger { _nextTaskId++; late final Task task; final completer = Completer(); - if (execution is Execute) { - task = TaskRegular(id: id, workPriority: priority, execution: execution, completer: completer); - } else if (execution is ExecuteWithPort) { + if (execution is ExecuteWithPort) { task = TaskWithPort( id: id, workPriority: priority, @@ -177,6 +180,8 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger { completer: completer, onMessage: onMessage!, ); + } else if (execution is Execute) { + task = TaskRegular(id: id, workPriority: priority, execution: execution, completer: completer); } _queue.add(task); _schedule(); @@ -199,7 +204,7 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger { if (_pool.every((worker) => worker.taskId != null)) { return; } - if (_dynamicSpawning) { + if (_dynamicSpawning && _queue.isNotEmpty) { final freeWorker = _pool.firstWhereOrNull( (worker) => worker.taskId == null && !worker.initialized && !worker.initializing, ); @@ -221,7 +226,7 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger { .work(task) .then( (value) { - //could be completed already by cancel and it is normal. + //might be completed by cancel and it is normal. //Assuming that worker finished with error and cleaned gracefully task.complete(value, null, null); }, diff --git a/mobile/mise.toml b/mobile/mise.toml index 88b8902053..6d6af62876 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -1,5 +1,5 @@ [tools] -flutter = "3.35.7" +flutter = "3.41.7" [tools."github:CQLabs/homebrew-dcm"] version = "1.30.0" @@ -40,7 +40,13 @@ depends = [ [tasks."codegen:translation"] alias = "translation" description = "Generate translations from i18n JSONs" -run = [{ task = "//:i18n:format-fix" }, { tasks = ["i18n:loader", "i18n:keys"] }] +run = [ + { task = "//:i18n:format-fix" }, + { tasks = [ + "i18n:loader", + "i18n:keys", + ] }, +] [tasks."codegen:app-icon"] description = "Generate app icons" diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index c864333780..50bbff2bae 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: 2.6.0 +- API version: 2.7.5 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen @@ -56,10 +56,10 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); final api_instance = APIKeysApi(); -final aPIKeyCreateDto = APIKeyCreateDto(); // APIKeyCreateDto | +final apiKeyCreateDto = ApiKeyCreateDto(); // ApiKeyCreateDto | try { - final result = api_instance.createApiKey(aPIKeyCreateDto); + final result = api_instance.createApiKey(apiKeyCreateDto); print(result); } catch (e) { print('Exception when calling APIKeysApi->createApiKey: $e\n'); @@ -89,6 +89,7 @@ Class | Method | HTTP request | Description *AlbumsApi* | [**createAlbum**](doc//AlbumsApi.md#createalbum) | **POST** /albums | Create an album *AlbumsApi* | [**deleteAlbum**](doc//AlbumsApi.md#deletealbum) | **DELETE** /albums/{id} | Delete an album *AlbumsApi* | [**getAlbumInfo**](doc//AlbumsApi.md#getalbuminfo) | **GET** /albums/{id} | Retrieve an album +*AlbumsApi* | [**getAlbumMapMarkers**](doc//AlbumsApi.md#getalbummapmarkers) | **GET** /albums/{id}/map-markers | Retrieve album map markers *AlbumsApi* | [**getAlbumStatistics**](doc//AlbumsApi.md#getalbumstatistics) | **GET** /albums/statistics | Retrieve album statistics *AlbumsApi* | [**getAllAlbums**](doc//AlbumsApi.md#getallalbums) | **GET** /albums | List all albums *AlbumsApi* | [**removeAssetFromAlbum**](doc//AlbumsApi.md#removeassetfromalbum) | **DELETE** /albums/{id}/assets | Remove assets from an album @@ -96,24 +97,20 @@ Class | Method | HTTP request | Description *AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} | Update an album *AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} | Update user role *AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | Check bulk upload -*AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | Check existing assets *AssetsApi* | [**copyAsset**](doc//AssetsApi.md#copyasset) | **PUT** /assets/copy | Copy asset *AssetsApi* | [**deleteAssetMetadata**](doc//AssetsApi.md#deleteassetmetadata) | **DELETE** /assets/{id}/metadata/{key} | Delete asset metadata by key *AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets | Delete assets *AssetsApi* | [**deleteBulkAssetMetadata**](doc//AssetsApi.md#deletebulkassetmetadata) | **DELETE** /assets/metadata | Delete asset metadata *AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original | Download original asset *AssetsApi* | [**editAsset**](doc//AssetsApi.md#editasset) | **PUT** /assets/{id}/edits | Apply edits to an existing asset -*AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID *AssetsApi* | [**getAssetEdits**](doc//AssetsApi.md#getassetedits) | **GET** /assets/{id}/edits | Retrieve edits for an existing asset *AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} | Retrieve an asset *AssetsApi* | [**getAssetMetadata**](doc//AssetsApi.md#getassetmetadata) | **GET** /assets/{id}/metadata | Get asset metadata *AssetsApi* | [**getAssetMetadataByKey**](doc//AssetsApi.md#getassetmetadatabykey) | **GET** /assets/{id}/metadata/{key} | Retrieve asset metadata by key *AssetsApi* | [**getAssetOcr**](doc//AssetsApi.md#getassetocr) | **GET** /assets/{id}/ocr | Retrieve asset OCR data *AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics | Get asset statistics -*AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random | Get random assets *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | Play asset video *AssetsApi* | [**removeAssetEdits**](doc//AssetsApi.md#removeassetedits) | **DELETE** /assets/{id}/edits | Remove edits from an existing asset -*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | Run an asset job *AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} | Update an asset *AssetsApi* | [**updateAssetMetadata**](doc//AssetsApi.md#updateassetmetadata) | **PUT** /assets/{id}/metadata | Update asset metadata @@ -129,6 +126,7 @@ Class | Method | HTTP request | Description *AuthenticationApi* | [**lockAuthSession**](doc//AuthenticationApi.md#lockauthsession) | **POST** /auth/session/lock | Lock auth session *AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login | Login *AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout | Logout +*AuthenticationApi* | [**logoutOAuth**](doc//AuthenticationApi.md#logoutoauth) | **POST** /oauth/backchannel-logout | Backchannel OAuth logout *AuthenticationApi* | [**redirectOAuthToMobile**](doc//AuthenticationApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect | Redirect OAuth to mobile *AuthenticationApi* | [**resetPinCode**](doc//AuthenticationApi.md#resetpincode) | **DELETE** /auth/pin-code | Reset pin code *AuthenticationApi* | [**setupPinCode**](doc//AuthenticationApi.md#setuppincode) | **POST** /auth/pin-code | Setup pin code @@ -144,18 +142,14 @@ Class | Method | HTTP request | Description *DatabaseBackupsAdminApi* | [**startDatabaseRestoreFlow**](doc//DatabaseBackupsAdminApi.md#startdatabaserestoreflow) | **POST** /admin/database-backups/start-restore | Start database backup restore flow *DatabaseBackupsAdminApi* | [**uploadDatabaseBackup**](doc//DatabaseBackupsAdminApi.md#uploaddatabasebackup) | **POST** /admin/database-backups/upload | Upload database backup *DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} | Create a partner -*DeprecatedApi* | [**getAllUserAssetsByDeviceId**](doc//DeprecatedApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID -*DeprecatedApi* | [**getDeltaSync**](doc//DeprecatedApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user -*DeprecatedApi* | [**getFullSyncForUser**](doc//DeprecatedApi.md#getfullsyncforuser) | **POST** /sync/full-sync | Get full sync for user *DeprecatedApi* | [**getQueuesLegacy**](doc//DeprecatedApi.md#getqueueslegacy) | **GET** /jobs | Retrieve queue counts and status -*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | Get random assets -*DeprecatedApi* | [**replaceAsset**](doc//DeprecatedApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset *DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information *DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | Delete a duplicate *DuplicatesApi* | [**deleteDuplicates**](doc//DuplicatesApi.md#deleteduplicates) | **DELETE** /duplicates | Delete duplicates *DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates | Retrieve duplicates +*DuplicatesApi* | [**resolveDuplicates**](doc//DuplicatesApi.md#resolveduplicates) | **POST** /duplicates/resolve | Resolve duplicate groups *FacesApi* | [**createFace**](doc//FacesApi.md#createface) | **POST** /faces | Create a face *FacesApi* | [**deleteFace**](doc//FacesApi.md#deleteface) | **DELETE** /faces/{id} | Delete a face *FacesApi* | [**getFaces**](doc//FacesApi.md#getfaces) | **GET** /faces | Retrieve faces for asset @@ -238,7 +232,6 @@ Class | Method | HTTP request | Description *ServerApi* | [**getServerVersion**](doc//ServerApi.md#getserverversion) | **GET** /server/version | Get server version *ServerApi* | [**getStorage**](doc//ServerApi.md#getstorage) | **GET** /server/storage | Get storage *ServerApi* | [**getSupportedMediaTypes**](doc//ServerApi.md#getsupportedmediatypes) | **GET** /server/media-types | Get supported media types -*ServerApi* | [**getTheme**](doc//ServerApi.md#gettheme) | **GET** /server/theme | Get theme *ServerApi* | [**getVersionCheck**](doc//ServerApi.md#getversioncheck) | **GET** /server/version-check | Get version check status *ServerApi* | [**getVersionHistory**](doc//ServerApi.md#getversionhistory) | **GET** /server/version-history | Get version history *ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping | Ping @@ -266,8 +259,6 @@ Class | Method | HTTP request | Description *StacksApi* | [**searchStacks**](doc//StacksApi.md#searchstacks) | **GET** /stacks | Retrieve stacks *StacksApi* | [**updateStack**](doc//StacksApi.md#updatestack) | **PUT** /stacks/{id} | Update a stack *SyncApi* | [**deleteSyncAck**](doc//SyncApi.md#deletesyncack) | **DELETE** /sync/ack | Delete acknowledgements -*SyncApi* | [**getDeltaSync**](doc//SyncApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user -*SyncApi* | [**getFullSyncForUser**](doc//SyncApi.md#getfullsyncforuser) | **POST** /sync/full-sync | Get full sync for user *SyncApi* | [**getSyncAck**](doc//SyncApi.md#getsyncack) | **GET** /sync/ack | Retrieve acknowledgements *SyncApi* | [**getSyncStream**](doc//SyncApi.md#getsyncstream) | **POST** /sync/stream | Stream sync changes *SyncApi* | [**sendSyncAck**](doc//SyncApi.md#sendsyncack) | **POST** /sync/ack | Acknowledge changes @@ -329,10 +320,6 @@ Class | Method | HTTP request | Description ## Documentation For Models - - [APIKeyCreateDto](doc//APIKeyCreateDto.md) - - [APIKeyCreateResponseDto](doc//APIKeyCreateResponseDto.md) - - [APIKeyResponseDto](doc//APIKeyResponseDto.md) - - [APIKeyUpdateDto](doc//APIKeyUpdateDto.md) - [ActivityCreateDto](doc//ActivityCreateDto.md) - [ActivityResponseDto](doc//ActivityResponseDto.md) - [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md) @@ -348,6 +335,10 @@ Class | Method | HTTP request | Description - [AlbumsAddAssetsResponseDto](doc//AlbumsAddAssetsResponseDto.md) - [AlbumsResponse](doc//AlbumsResponse.md) - [AlbumsUpdate](doc//AlbumsUpdate.md) + - [ApiKeyCreateDto](doc//ApiKeyCreateDto.md) + - [ApiKeyCreateResponseDto](doc//ApiKeyCreateResponseDto.md) + - [ApiKeyResponseDto](doc//ApiKeyResponseDto.md) + - [ApiKeyUpdateDto](doc//ApiKeyUpdateDto.md) - [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md) - [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md) - [AssetBulkUploadCheckDto](doc//AssetBulkUploadCheckDto.md) @@ -355,8 +346,6 @@ Class | Method | HTTP request | Description - [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md) - [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md) - [AssetCopyDto](doc//AssetCopyDto.md) - - [AssetDeltaSyncDto](doc//AssetDeltaSyncDto.md) - - [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md) - [AssetEditAction](doc//AssetEditAction.md) - [AssetEditActionItemDto](doc//AssetEditActionItemDto.md) - [AssetEditActionItemDtoParameters](doc//AssetEditActionItemDtoParameters.md) @@ -369,7 +358,7 @@ Class | Method | HTTP request | Description - [AssetFaceUpdateDto](doc//AssetFaceUpdateDto.md) - [AssetFaceUpdateItem](doc//AssetFaceUpdateItem.md) - [AssetFaceWithoutPersonResponseDto](doc//AssetFaceWithoutPersonResponseDto.md) - - [AssetFullSyncDto](doc//AssetFullSyncDto.md) + - [AssetIdErrorReason](doc//AssetIdErrorReason.md) - [AssetIdsDto](doc//AssetIdsDto.md) - [AssetIdsResponseDto](doc//AssetIdsResponseDto.md) - [AssetJobName](doc//AssetJobName.md) @@ -387,10 +376,12 @@ Class | Method | HTTP request | Description - [AssetMetadataUpsertItemDto](doc//AssetMetadataUpsertItemDto.md) - [AssetOcrResponseDto](doc//AssetOcrResponseDto.md) - [AssetOrder](doc//AssetOrder.md) + - [AssetRejectReason](doc//AssetRejectReason.md) - [AssetResponseDto](doc//AssetResponseDto.md) - [AssetStackResponseDto](doc//AssetStackResponseDto.md) - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md) - [AssetTypeEnum](doc//AssetTypeEnum.md) + - [AssetUploadAction](doc//AssetUploadAction.md) - [AssetVisibility](doc//AssetVisibility.md) - [AudioCodec](doc//AudioCodec.md) - [AuthStatusResponseDto](doc//AuthStatusResponseDto.md) @@ -403,8 +394,6 @@ Class | Method | HTTP request | Description - [CastResponse](doc//CastResponse.md) - [CastUpdate](doc//CastUpdate.md) - [ChangePasswordDto](doc//ChangePasswordDto.md) - - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md) - - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md) - [Colorspace](doc//Colorspace.md) - [ContributorCountResponseDto](doc//ContributorCountResponseDto.md) - [CreateAlbumDto](doc//CreateAlbumDto.md) @@ -422,6 +411,8 @@ Class | Method | HTTP request | Description - [DownloadResponseDto](doc//DownloadResponseDto.md) - [DownloadUpdate](doc//DownloadUpdate.md) - [DuplicateDetectionConfig](doc//DuplicateDetectionConfig.md) + - [DuplicateResolveDto](doc//DuplicateResolveDto.md) + - [DuplicateResolveGroupDto](doc//DuplicateResolveGroupDto.md) - [DuplicateResponseDto](doc//DuplicateResponseDto.md) - [EmailNotificationsResponse](doc//EmailNotificationsResponse.md) - [EmailNotificationsUpdate](doc//EmailNotificationsUpdate.md) @@ -437,7 +428,6 @@ Class | Method | HTTP request | Description - [LibraryResponseDto](doc//LibraryResponseDto.md) - [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md) - [LicenseKeyDto](doc//LicenseKeyDto.md) - - [LicenseResponseDto](doc//LicenseResponseDto.md) - [LogLevel](doc//LogLevel.md) - [LoginCredentialDto](doc//LoginCredentialDto.md) - [LoginResponseDto](doc//LoginResponseDto.md) @@ -501,6 +491,10 @@ Class | Method | HTTP request | Description - [PluginActionResponseDto](doc//PluginActionResponseDto.md) - [PluginContextType](doc//PluginContextType.md) - [PluginFilterResponseDto](doc//PluginFilterResponseDto.md) + - [PluginJsonSchema](doc//PluginJsonSchema.md) + - [PluginJsonSchemaProperty](doc//PluginJsonSchemaProperty.md) + - [PluginJsonSchemaPropertyAdditionalProperties](doc//PluginJsonSchemaPropertyAdditionalProperties.md) + - [PluginJsonSchemaType](doc//PluginJsonSchemaType.md) - [PluginResponseDto](doc//PluginResponseDto.md) - [PluginTriggerResponseDto](doc//PluginTriggerResponseDto.md) - [PluginTriggerType](doc//PluginTriggerType.md) @@ -542,7 +536,6 @@ Class | Method | HTTP request | Description - [ServerPingResponse](doc//ServerPingResponse.md) - [ServerStatsResponseDto](doc//ServerStatsResponseDto.md) - [ServerStorageResponseDto](doc//ServerStorageResponseDto.md) - - [ServerThemeDto](doc//ServerThemeDto.md) - [ServerVersionHistoryResponseDto](doc//ServerVersionHistoryResponseDto.md) - [ServerVersionResponseDto](doc//ServerVersionResponseDto.md) - [SessionCreateDto](doc//SessionCreateDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 253e8a6811..9eca7a2ab7 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -68,10 +68,6 @@ part 'api/users_admin_api.dart'; part 'api/views_api.dart'; part 'api/workflows_api.dart'; -part 'model/api_key_create_dto.dart'; -part 'model/api_key_create_response_dto.dart'; -part 'model/api_key_response_dto.dart'; -part 'model/api_key_update_dto.dart'; part 'model/activity_create_dto.dart'; part 'model/activity_response_dto.dart'; part 'model/activity_statistics_response_dto.dart'; @@ -87,6 +83,10 @@ part 'model/albums_add_assets_dto.dart'; part 'model/albums_add_assets_response_dto.dart'; part 'model/albums_response.dart'; part 'model/albums_update.dart'; +part 'model/api_key_create_dto.dart'; +part 'model/api_key_create_response_dto.dart'; +part 'model/api_key_response_dto.dart'; +part 'model/api_key_update_dto.dart'; part 'model/asset_bulk_delete_dto.dart'; part 'model/asset_bulk_update_dto.dart'; part 'model/asset_bulk_upload_check_dto.dart'; @@ -94,8 +94,6 @@ part 'model/asset_bulk_upload_check_item.dart'; part 'model/asset_bulk_upload_check_response_dto.dart'; part 'model/asset_bulk_upload_check_result.dart'; part 'model/asset_copy_dto.dart'; -part 'model/asset_delta_sync_dto.dart'; -part 'model/asset_delta_sync_response_dto.dart'; part 'model/asset_edit_action.dart'; part 'model/asset_edit_action_item_dto.dart'; part 'model/asset_edit_action_item_dto_parameters.dart'; @@ -108,7 +106,7 @@ part 'model/asset_face_response_dto.dart'; part 'model/asset_face_update_dto.dart'; part 'model/asset_face_update_item.dart'; part 'model/asset_face_without_person_response_dto.dart'; -part 'model/asset_full_sync_dto.dart'; +part 'model/asset_id_error_reason.dart'; part 'model/asset_ids_dto.dart'; part 'model/asset_ids_response_dto.dart'; part 'model/asset_job_name.dart'; @@ -126,10 +124,12 @@ part 'model/asset_metadata_upsert_dto.dart'; part 'model/asset_metadata_upsert_item_dto.dart'; part 'model/asset_ocr_response_dto.dart'; part 'model/asset_order.dart'; +part 'model/asset_reject_reason.dart'; part 'model/asset_response_dto.dart'; part 'model/asset_stack_response_dto.dart'; part 'model/asset_stats_response_dto.dart'; part 'model/asset_type_enum.dart'; +part 'model/asset_upload_action.dart'; part 'model/asset_visibility.dart'; part 'model/audio_codec.dart'; part 'model/auth_status_response_dto.dart'; @@ -142,8 +142,6 @@ part 'model/cq_mode.dart'; part 'model/cast_response.dart'; part 'model/cast_update.dart'; part 'model/change_password_dto.dart'; -part 'model/check_existing_assets_dto.dart'; -part 'model/check_existing_assets_response_dto.dart'; part 'model/colorspace.dart'; part 'model/contributor_count_response_dto.dart'; part 'model/create_album_dto.dart'; @@ -161,6 +159,8 @@ part 'model/download_response.dart'; part 'model/download_response_dto.dart'; part 'model/download_update.dart'; part 'model/duplicate_detection_config.dart'; +part 'model/duplicate_resolve_dto.dart'; +part 'model/duplicate_resolve_group_dto.dart'; part 'model/duplicate_response_dto.dart'; part 'model/email_notifications_response.dart'; part 'model/email_notifications_update.dart'; @@ -176,7 +176,6 @@ part 'model/job_settings_dto.dart'; part 'model/library_response_dto.dart'; part 'model/library_stats_response_dto.dart'; part 'model/license_key_dto.dart'; -part 'model/license_response_dto.dart'; part 'model/log_level.dart'; part 'model/login_credential_dto.dart'; part 'model/login_response_dto.dart'; @@ -240,6 +239,10 @@ part 'model/places_response_dto.dart'; part 'model/plugin_action_response_dto.dart'; part 'model/plugin_context_type.dart'; part 'model/plugin_filter_response_dto.dart'; +part 'model/plugin_json_schema.dart'; +part 'model/plugin_json_schema_property.dart'; +part 'model/plugin_json_schema_property_additional_properties.dart'; +part 'model/plugin_json_schema_type.dart'; part 'model/plugin_response_dto.dart'; part 'model/plugin_trigger_response_dto.dart'; part 'model/plugin_trigger_type.dart'; @@ -281,7 +284,6 @@ part 'model/server_media_types_response_dto.dart'; part 'model/server_ping_response.dart'; part 'model/server_stats_response_dto.dart'; part 'model/server_storage_response_dto.dart'; -part 'model/server_theme_dto.dart'; part 'model/server_version_history_response_dto.dart'; part 'model/server_version_response_dto.dart'; part 'model/session_create_dto.dart'; diff --git a/mobile/openapi/lib/api/activities_api.dart b/mobile/openapi/lib/api/activities_api.dart index 697598ac97..e0a393948c 100644 --- a/mobile/openapi/lib/api/activities_api.dart +++ b/mobile/openapi/lib/api/activities_api.dart @@ -136,10 +136,8 @@ class ActivitiesApi { /// Asset ID (if activity is for an asset) /// /// * [ReactionLevel] level: - /// Filter by activity level /// /// * [ReactionType] type: - /// Filter by activity type /// /// * [String] userId: /// Filter by user ID @@ -195,10 +193,8 @@ class ActivitiesApi { /// Asset ID (if activity is for an asset) /// /// * [ReactionLevel] level: - /// Filter by activity level /// /// * [ReactionType] type: - /// Filter by activity type /// /// * [String] userId: /// Filter by user ID diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index e2db95b9e0..d08d1cba9d 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -27,11 +27,7 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { String? key, String? slug, }) async { + Future addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/assets' .replaceAll('{id}', id); @@ -43,13 +39,6 @@ class AlbumsApi { final headerParams = {}; final formParams = {}; - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - const contentTypes = ['application/json']; @@ -73,12 +62,8 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto, { String? key, String? slug, }) async { - final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto, key: key, slug: slug, ); + Future?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto,) async { + final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -104,11 +89,7 @@ class AlbumsApi { /// Parameters: /// /// * [AlbumsAddAssetsDto] albumsAddAssetsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addAssetsToAlbumsWithHttpInfo(AlbumsAddAssetsDto albumsAddAssetsDto, { String? key, String? slug, }) async { + Future addAssetsToAlbumsWithHttpInfo(AlbumsAddAssetsDto albumsAddAssetsDto,) async { // ignore: prefer_const_declarations final apiPath = r'/albums/assets'; @@ -119,13 +100,6 @@ class AlbumsApi { final headerParams = {}; final formParams = {}; - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - const contentTypes = ['application/json']; @@ -147,12 +121,8 @@ class AlbumsApi { /// Parameters: /// /// * [AlbumsAddAssetsDto] albumsAddAssetsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addAssetsToAlbums(AlbumsAddAssetsDto albumsAddAssetsDto, { String? key, String? slug, }) async { - final response = await addAssetsToAlbumsWithHttpInfo(albumsAddAssetsDto, key: key, slug: slug, ); + Future addAssetsToAlbums(AlbumsAddAssetsDto albumsAddAssetsDto,) async { + final response = await addAssetsToAlbumsWithHttpInfo(albumsAddAssetsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -345,10 +315,7 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - /// - /// * [bool] withoutAssets: - /// Exclude assets from response - Future getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async { + Future getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}' .replaceAll('{id}', id); @@ -366,9 +333,6 @@ class AlbumsApi { if (slug != null) { queryParams.addAll(_queryParams('', 'slug', slug)); } - if (withoutAssets != null) { - queryParams.addAll(_queryParams('', 'withoutAssets', withoutAssets)); - } const contentTypes = []; @@ -395,11 +359,8 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - /// - /// * [bool] withoutAssets: - /// Exclude assets from response - Future getAlbumInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async { - final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, withoutAssets: withoutAssets, ); + Future getAlbumInfo(String id, { String? key, String? slug, }) async { + final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -413,6 +374,81 @@ class AlbumsApi { return null; } + /// Retrieve album map markers + /// + /// Retrieve map marker information for a specific album by its ID. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getAlbumMapMarkersWithHttpInfo(String id, { String? key, String? slug, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/albums/{id}/map-markers' + .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 = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Retrieve album map markers + /// + /// Retrieve map marker information for a specific album by its ID. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future?> getAlbumMapMarkers(String id, { String? key, String? slug, }) async { + final response = await getAlbumMapMarkersWithHttpInfo(id, key: key, slug: slug, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + /// Retrieve album statistics /// /// Returns statistics about the albums available to the authenticated user. diff --git a/mobile/openapi/lib/api/api_keys_api.dart b/mobile/openapi/lib/api/api_keys_api.dart index 0bd26575c6..3ca85265c4 100644 --- a/mobile/openapi/lib/api/api_keys_api.dart +++ b/mobile/openapi/lib/api/api_keys_api.dart @@ -24,13 +24,13 @@ class APIKeysApi { /// /// Parameters: /// - /// * [APIKeyCreateDto] aPIKeyCreateDto (required): - Future createApiKeyWithHttpInfo(APIKeyCreateDto aPIKeyCreateDto,) async { + /// * [ApiKeyCreateDto] apiKeyCreateDto (required): + Future createApiKeyWithHttpInfo(ApiKeyCreateDto apiKeyCreateDto,) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys'; // ignore: prefer_final_locals - Object? postBody = aPIKeyCreateDto; + Object? postBody = apiKeyCreateDto; final queryParams = []; final headerParams = {}; @@ -56,9 +56,9 @@ class APIKeysApi { /// /// Parameters: /// - /// * [APIKeyCreateDto] aPIKeyCreateDto (required): - Future createApiKey(APIKeyCreateDto aPIKeyCreateDto,) async { - final response = await createApiKeyWithHttpInfo(aPIKeyCreateDto,); + /// * [ApiKeyCreateDto] apiKeyCreateDto (required): + Future createApiKey(ApiKeyCreateDto apiKeyCreateDto,) async { + final response = await createApiKeyWithHttpInfo(apiKeyCreateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -66,7 +66,7 @@ class APIKeysApi { // 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), 'APIKeyCreateResponseDto',) as APIKeyCreateResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyCreateResponseDto',) as ApiKeyCreateResponseDto; } return null; @@ -163,7 +163,7 @@ class APIKeysApi { /// Parameters: /// /// * [String] id (required): - Future getApiKey(String id,) async { + Future getApiKey(String id,) async { final response = await getApiKeyWithHttpInfo(id,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -172,7 +172,7 @@ class APIKeysApi { // 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), 'APIKeyResponseDto',) as APIKeyResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyResponseDto',) as ApiKeyResponseDto; } return null; @@ -211,7 +211,7 @@ class APIKeysApi { /// List all API keys /// /// Retrieve all API keys of the current user. - Future?> getApiKeys() async { + Future?> getApiKeys() async { final response = await getApiKeysWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -221,8 +221,8 @@ class APIKeysApi { // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() .toList(growable: false); } @@ -262,7 +262,7 @@ class APIKeysApi { /// Retrieve the current API key /// /// Retrieve the API key that is used to access this endpoint. - Future getMyApiKey() async { + Future getMyApiKey() async { final response = await getMyApiKeyWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -271,7 +271,7 @@ class APIKeysApi { // 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), 'APIKeyResponseDto',) as APIKeyResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyResponseDto',) as ApiKeyResponseDto; } return null; @@ -287,14 +287,14 @@ class APIKeysApi { /// /// * [String] id (required): /// - /// * [APIKeyUpdateDto] aPIKeyUpdateDto (required): - Future updateApiKeyWithHttpInfo(String id, APIKeyUpdateDto aPIKeyUpdateDto,) async { + /// * [ApiKeyUpdateDto] apiKeyUpdateDto (required): + Future updateApiKeyWithHttpInfo(String id, ApiKeyUpdateDto apiKeyUpdateDto,) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys/{id}' .replaceAll('{id}', id); // ignore: prefer_final_locals - Object? postBody = aPIKeyUpdateDto; + Object? postBody = apiKeyUpdateDto; final queryParams = []; final headerParams = {}; @@ -322,9 +322,9 @@ class APIKeysApi { /// /// * [String] id (required): /// - /// * [APIKeyUpdateDto] aPIKeyUpdateDto (required): - Future updateApiKey(String id, APIKeyUpdateDto aPIKeyUpdateDto,) async { - final response = await updateApiKeyWithHttpInfo(id, aPIKeyUpdateDto,); + /// * [ApiKeyUpdateDto] apiKeyUpdateDto (required): + Future updateApiKey(String id, ApiKeyUpdateDto apiKeyUpdateDto,) async { + final response = await updateApiKeyWithHttpInfo(id, apiKeyUpdateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -332,7 +332,7 @@ class APIKeysApi { // 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), 'APIKeyResponseDto',) as APIKeyResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyResponseDto',) as ApiKeyResponseDto; } return null; diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index a026b99028..5046376168 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -72,62 +72,6 @@ class AssetsApi { return null; } - /// Check existing assets - /// - /// Checks if multiple assets exist on the server and returns all existing - used by background backup - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [CheckExistingAssetsDto] checkExistingAssetsDto (required): - Future checkExistingAssetsWithHttpInfo(CheckExistingAssetsDto checkExistingAssetsDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/exist'; - - // ignore: prefer_final_locals - Object? postBody = checkExistingAssetsDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Check existing assets - /// - /// Checks if multiple assets exist on the server and returns all existing - used by background backup - /// - /// Parameters: - /// - /// * [CheckExistingAssetsDto] checkExistingAssetsDto (required): - Future checkExistingAssets(CheckExistingAssetsDto checkExistingAssetsDto,) async { - final response = await checkExistingAssetsWithHttpInfo(checkExistingAssetsDto,); - 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), 'CheckExistingAssetsResponseDto',) as CheckExistingAssetsResponseDto; - - } - return null; - } - /// Copy asset /// /// Copy asset information like albums, tags, etc. from one asset to another. @@ -472,68 +416,6 @@ class AssetsApi { return null; } - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future getAllUserAssetsByDeviceIdWithHttpInfo(String deviceId,) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/device/{deviceId}' - .replaceAll('{deviceId}', deviceId); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future?> getAllUserAssetsByDeviceId(String deviceId,) async { - final response = await getAllUserAssetsByDeviceIdWithHttpInfo(deviceId,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Retrieve edits for an existing asset /// /// Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset. @@ -864,7 +746,6 @@ class AssetsApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getAssetStatisticsWithHttpInfo({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/statistics'; @@ -913,7 +794,6 @@ class AssetsApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getAssetStatistics({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { final response = await getAssetStatisticsWithHttpInfo( isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -929,71 +809,6 @@ class AssetsApi { return null; } - /// Get random assets - /// - /// Retrieve a specified number of random assets for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [num] count: - /// Number of random assets to return - Future getRandomWithHttpInfo({ num? count, }) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/random'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (count != null) { - queryParams.addAll(_queryParams('', 'count', count)); - } - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get random assets - /// - /// Retrieve a specified number of random assets for the authenticated user. - /// - /// Parameters: - /// - /// * [num] count: - /// Number of random assets to return - Future?> getRandom({ num? count, }) async { - final response = await getRandomWithHttpInfo( count: count, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Play asset video /// /// Streams the video file for the specified asset. This endpoint also supports byte range requests. @@ -1115,154 +930,6 @@ class AssetsApi { } } - /// Replace asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// 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 asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// 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; - } - /// Run an asset job /// /// Run a specific job on a set of assets. @@ -1554,12 +1221,6 @@ class AssetsApi { /// * [MultipartFile] assetData (required): /// Asset file data /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// /// * [DateTime] fileCreatedAt (required): /// File creation date /// @@ -1592,8 +1253,7 @@ class AssetsApi { /// Sidecar file data /// /// * [AssetVisibility] visibility: - /// Asset visibility - Future uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { + Future uploadAssetWithHttpInfo(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets'; @@ -1624,14 +1284,6 @@ class AssetsApi { 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); @@ -1693,12 +1345,6 @@ class AssetsApi { /// * [MultipartFile] assetData (required): /// Asset file data /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// /// * [DateTime] fileCreatedAt (required): /// File creation date /// @@ -1731,9 +1377,8 @@ class AssetsApi { /// Sidecar file data /// /// * [AssetVisibility] visibility: - /// Asset visibility - Future uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { - final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, ); + Future uploadAsset(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { + final response = await uploadAssetWithHttpInfo(assetData, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1763,7 +1408,6 @@ class AssetsApi { /// * [String] key: /// /// * [AssetMediaSize] size: - /// Asset media size /// /// * [String] slug: Future viewAssetWithHttpInfo(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { @@ -1819,7 +1463,6 @@ class AssetsApi { /// * [String] key: /// /// * [AssetMediaSize] size: - /// Asset media size /// /// * [String] slug: Future viewAsset(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { diff --git a/mobile/openapi/lib/api/authentication_api.dart b/mobile/openapi/lib/api/authentication_api.dart index 52d46a525b..e1219f2c03 100644 --- a/mobile/openapi/lib/api/authentication_api.dart +++ b/mobile/openapi/lib/api/authentication_api.dart @@ -424,6 +424,59 @@ class AuthenticationApi { return null; } + /// Backchannel OAuth logout + /// + /// Logout the OAuth account and invalidate the session specified by the sid claim or all sessions if the sid claim is not present. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] logoutToken (required): + /// OAuth logout token + Future logoutOAuthWithHttpInfo(String logoutToken,) async { + // ignore: prefer_const_declarations + final apiPath = r'/oauth/backchannel-logout'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/x-www-form-urlencoded']; + + if (logoutToken != null) { + formParams[r'logout_token'] = parameterToString(logoutToken); + } + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Backchannel OAuth logout + /// + /// Logout the OAuth account and invalidate the session specified by the sid claim or all sessions if the sid claim is not present. + /// + /// Parameters: + /// + /// * [String] logoutToken (required): + /// OAuth logout token + Future logoutOAuth(String logoutToken,) async { + final response = await logoutOAuthWithHttpInfo(logoutToken,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Redirect OAuth to mobile /// /// Requests to this URL are automatically forwarded to the mobile app, and is used in some cases for OAuth redirecting. diff --git a/mobile/openapi/lib/api/database_backups_admin_api.dart b/mobile/openapi/lib/api/database_backups_admin_api.dart index fbd485f86f..768185db1e 100644 --- a/mobile/openapi/lib/api/database_backups_admin_api.dart +++ b/mobile/openapi/lib/api/database_backups_admin_api.dart @@ -218,6 +218,7 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [MultipartFile] file: + /// Database backup file Future uploadDatabaseBackupWithHttpInfo({ MultipartFile? file, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/database-backups/upload'; @@ -260,6 +261,7 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [MultipartFile] file: + /// Database backup file Future uploadDatabaseBackup({ MultipartFile? file, }) async { final response = await uploadDatabaseBackupWithHttpInfo( file: file, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index 33bcaf062c..a437cd5837 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -73,183 +73,6 @@ class DeprecatedApi { return null; } - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future getAllUserAssetsByDeviceIdWithHttpInfo(String deviceId,) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/device/{deviceId}' - .replaceAll('{deviceId}', deviceId); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future?> getAllUserAssetsByDeviceId(String deviceId,) async { - final response = await getAllUserAssetsByDeviceIdWithHttpInfo(deviceId,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSyncWithHttpInfo(AssetDeltaSyncDto assetDeltaSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/delta-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetDeltaSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSync(AssetDeltaSyncDto assetDeltaSyncDto,) async { - final response = await getDeltaSyncWithHttpInfo(assetDeltaSyncDto,); - 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), 'AssetDeltaSyncResponseDto',) as AssetDeltaSyncResponseDto; - - } - return null; - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future getFullSyncForUserWithHttpInfo(AssetFullSyncDto assetFullSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/full-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetFullSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future?> getFullSyncForUser(AssetFullSyncDto assetFullSyncDto,) async { - final response = await getFullSyncForUserWithHttpInfo(assetFullSyncDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Retrieve queue counts and status /// /// Retrieve the counts of the current queue, as well as the current status. @@ -298,219 +121,6 @@ class DeprecatedApi { return null; } - /// Get random assets - /// - /// Retrieve a specified number of random assets for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [num] count: - /// Number of random assets to return - Future getRandomWithHttpInfo({ num? count, }) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/random'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (count != null) { - queryParams.addAll(_queryParams('', 'count', count)); - } - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get random assets - /// - /// Retrieve a specified number of random assets for the authenticated user. - /// - /// Parameters: - /// - /// * [num] count: - /// Number of random assets to return - Future?> getRandom({ num? count, }) async { - final response = await getRandomWithHttpInfo( count: count, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - - /// Replace asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// 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 asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// 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; - } - /// Run jobs /// /// Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets. @@ -520,7 +130,6 @@ class DeprecatedApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { @@ -556,7 +165,6 @@ class DeprecatedApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { diff --git a/mobile/openapi/lib/api/duplicates_api.dart b/mobile/openapi/lib/api/duplicates_api.dart index 7fa7b368b5..e873537592 100644 --- a/mobile/openapi/lib/api/duplicates_api.dart +++ b/mobile/openapi/lib/api/duplicates_api.dart @@ -163,4 +163,63 @@ class DuplicatesApi { } return null; } + + /// Resolve duplicate groups + /// + /// Resolve duplicate groups by synchronizing metadata across assets and deleting/trashing duplicates. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [DuplicateResolveDto] duplicateResolveDto (required): + Future resolveDuplicatesWithHttpInfo(DuplicateResolveDto duplicateResolveDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/duplicates/resolve'; + + // ignore: prefer_final_locals + Object? postBody = duplicateResolveDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Resolve duplicate groups + /// + /// Resolve duplicate groups by synchronizing metadata across assets and deleting/trashing duplicates. + /// + /// Parameters: + /// + /// * [DuplicateResolveDto] duplicateResolveDto (required): + Future?> resolveDuplicates(DuplicateResolveDto duplicateResolveDto,) async { + final response = await resolveDuplicatesWithHttpInfo(duplicateResolveDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } } diff --git a/mobile/openapi/lib/api/jobs_api.dart b/mobile/openapi/lib/api/jobs_api.dart index 41517f8144..9dda59a883 100644 --- a/mobile/openapi/lib/api/jobs_api.dart +++ b/mobile/openapi/lib/api/jobs_api.dart @@ -121,7 +121,6 @@ class JobsApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { @@ -157,7 +156,6 @@ class JobsApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { diff --git a/mobile/openapi/lib/api/memories_api.dart b/mobile/openapi/lib/api/memories_api.dart index 913205428e..0cd96ac442 100644 --- a/mobile/openapi/lib/api/memories_api.dart +++ b/mobile/openapi/lib/api/memories_api.dart @@ -260,13 +260,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/statistics'; @@ -327,13 +325,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future memoriesStatistics({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { final response = await memoriesStatisticsWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -431,13 +427,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future searchMemoriesWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories'; @@ -498,13 +492,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { final response = await searchMemoriesWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/notifications_api.dart b/mobile/openapi/lib/api/notifications_api.dart index d4e2b1d80f..ab0be3e8f3 100644 --- a/mobile/openapi/lib/api/notifications_api.dart +++ b/mobile/openapi/lib/api/notifications_api.dart @@ -182,10 +182,8 @@ class NotificationsApi { /// Filter by notification ID /// /// * [NotificationLevel] level: - /// Filter by notification level /// /// * [NotificationType] type: - /// Filter by notification type /// /// * [bool] unread: /// Filter by unread status @@ -237,10 +235,8 @@ class NotificationsApi { /// Filter by notification ID /// /// * [NotificationLevel] level: - /// Filter by notification level /// /// * [NotificationType] type: - /// Filter by notification type /// /// * [bool] unread: /// Filter by unread status diff --git a/mobile/openapi/lib/api/partners_api.dart b/mobile/openapi/lib/api/partners_api.dart index 3b15b90909..7d18f6d867 100644 --- a/mobile/openapi/lib/api/partners_api.dart +++ b/mobile/openapi/lib/api/partners_api.dart @@ -138,7 +138,6 @@ class PartnersApi { /// Parameters: /// /// * [PartnerDirection] direction (required): - /// Partner direction Future getPartnersWithHttpInfo(PartnerDirection direction,) async { // ignore: prefer_const_declarations final apiPath = r'/partners'; @@ -173,7 +172,6 @@ class PartnersApi { /// Parameters: /// /// * [PartnerDirection] direction (required): - /// Partner direction Future?> getPartners(PartnerDirection direction,) async { final response = await getPartnersWithHttpInfo(direction,); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/queues_api.dart b/mobile/openapi/lib/api/queues_api.dart index ecb556e434..1312cb5952 100644 --- a/mobile/openapi/lib/api/queues_api.dart +++ b/mobile/openapi/lib/api/queues_api.dart @@ -25,7 +25,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueDeleteDto] queueDeleteDto (required): Future emptyQueueWithHttpInfo(QueueName name, QueueDeleteDto queueDeleteDto,) async { @@ -61,7 +60,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueDeleteDto] queueDeleteDto (required): Future emptyQueue(QueueName name, QueueDeleteDto queueDeleteDto,) async { @@ -80,7 +78,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name Future getQueueWithHttpInfo(QueueName name,) async { // ignore: prefer_const_declarations final apiPath = r'/queues/{name}' @@ -114,7 +111,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name Future getQueue(QueueName name,) async { final response = await getQueueWithHttpInfo(name,); if (response.statusCode >= HttpStatus.badRequest) { @@ -139,7 +135,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [List] status: /// Filter jobs by status @@ -180,7 +175,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [List] status: /// Filter jobs by status @@ -262,7 +256,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueUpdateDto] queueUpdateDto (required): Future updateQueueWithHttpInfo(QueueName name, QueueUpdateDto queueUpdateDto,) async { @@ -298,7 +291,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueUpdateDto] queueUpdateDto (required): Future updateQueue(QueueName name, QueueUpdateDto queueUpdateDto,) async { diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 085958de66..730627d4a1 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -127,7 +127,6 @@ class SearchApi { /// Parameters: /// /// * [SearchSuggestionType] type (required): - /// Suggestion type /// /// * [String] country: /// Filter by country @@ -198,7 +197,6 @@ class SearchApi { /// Parameters: /// /// * [SearchSuggestionType] type (required): - /// Suggestion type /// /// * [String] country: /// Filter by country @@ -370,9 +368,6 @@ class SearchApi { /// * [DateTime] createdBefore: /// Filter by creation date (before) /// - /// * [String] deviceId: - /// Device ID to filter by - /// /// * [bool] isEncoded: /// Filter by encoded status /// @@ -434,7 +429,6 @@ class SearchApi { /// Filter by trash date (before) /// /// * [AssetTypeEnum] type: - /// Asset type filter /// /// * [DateTime] updatedAfter: /// Filter by update date (after) @@ -443,14 +437,13 @@ class SearchApi { /// Filter by update date (before) /// /// * [AssetVisibility] visibility: - /// Filter by visibility /// /// * [bool] withDeleted: /// Include deleted assets /// /// * [bool] withExif: /// Include EXIF data in response - Future searchLargeAssetsWithHttpInfo({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { + Future searchLargeAssetsWithHttpInfo({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/large-assets'; @@ -476,9 +469,6 @@ class SearchApi { if (createdBefore != null) { queryParams.addAll(_queryParams('', 'createdBefore', createdBefore)); } - if (deviceId != null) { - queryParams.addAll(_queryParams('', 'deviceId', deviceId)); - } if (isEncoded != null) { queryParams.addAll(_queryParams('', 'isEncoded', isEncoded)); } @@ -593,9 +583,6 @@ class SearchApi { /// * [DateTime] createdBefore: /// Filter by creation date (before) /// - /// * [String] deviceId: - /// Device ID to filter by - /// /// * [bool] isEncoded: /// Filter by encoded status /// @@ -657,7 +644,6 @@ class SearchApi { /// Filter by trash date (before) /// /// * [AssetTypeEnum] type: - /// Asset type filter /// /// * [DateTime] updatedAfter: /// Filter by update date (after) @@ -666,15 +652,14 @@ class SearchApi { /// Filter by update date (before) /// /// * [AssetVisibility] visibility: - /// Filter by visibility /// /// * [bool] withDeleted: /// Include deleted assets /// /// * [bool] withExif: /// Include EXIF data in response - Future?> searchLargeAssets({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { - final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceId: deviceId, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, 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, ); + Future?> searchLargeAssets({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { + final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, 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, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/server_api.dart b/mobile/openapi/lib/api/server_api.dart index f5b70a9ea4..dd38ade167 100644 --- a/mobile/openapi/lib/api/server_api.dart +++ b/mobile/openapi/lib/api/server_api.dart @@ -281,7 +281,7 @@ class ServerApi { /// Get product key /// /// Retrieve information about whether the server currently has a product key registered. - Future getServerLicense() async { + Future getServerLicense() async { final response = await getServerLicenseWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -290,7 +290,7 @@ class ServerApi { // 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), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; @@ -488,54 +488,6 @@ class ServerApi { return null; } - /// Get theme - /// - /// Retrieve the custom CSS, if existent. - /// - /// Note: This method returns the HTTP [Response]. - Future getThemeWithHttpInfo() async { - // ignore: prefer_const_declarations - final apiPath = r'/server/theme'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get theme - /// - /// Retrieve the custom CSS, if existent. - Future getTheme() async { - final response = await getThemeWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerThemeDto',) as ServerThemeDto; - - } - return null; - } - /// Get version check status /// /// Retrieve information about the last time the version check ran. @@ -724,7 +676,7 @@ class ServerApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setServerLicense(LicenseKeyDto licenseKeyDto,) async { + Future setServerLicense(LicenseKeyDto licenseKeyDto,) async { final response = await setServerLicenseWithHttpInfo(licenseKeyDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -733,7 +685,7 @@ class ServerApi { // 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), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; diff --git a/mobile/openapi/lib/api/shared_links_api.dart b/mobile/openapi/lib/api/shared_links_api.dart index 084662ace8..4750442287 100644 --- a/mobile/openapi/lib/api/shared_links_api.dart +++ b/mobile/openapi/lib/api/shared_links_api.dart @@ -27,11 +27,7 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async { + Future addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/{id}/assets' .replaceAll('{id}', id); @@ -43,13 +39,6 @@ class SharedLinksApi { final headerParams = {}; final formParams = {}; - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - const contentTypes = ['application/json']; @@ -73,12 +62,8 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async { - final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto, key: key, slug: slug, ); + Future?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto,) async { + final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -235,14 +220,8 @@ class SharedLinksApi { /// /// * [String] key: /// - /// * [String] password: - /// Link password - /// /// * [String] slug: - /// - /// * [String] token: - /// Access token - Future getMySharedLinkWithHttpInfo({ String? key, String? password, String? slug, String? token, }) async { + Future getMySharedLinkWithHttpInfo({ String? key, String? slug, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/me'; @@ -256,15 +235,9 @@ class SharedLinksApi { if (key != null) { queryParams.addAll(_queryParams('', 'key', key)); } - if (password != null) { - queryParams.addAll(_queryParams('', 'password', password)); - } if (slug != null) { queryParams.addAll(_queryParams('', 'slug', slug)); } - if (token != null) { - queryParams.addAll(_queryParams('', 'token', token)); - } const contentTypes = []; @@ -288,15 +261,9 @@ class SharedLinksApi { /// /// * [String] key: /// - /// * [String] password: - /// Link password - /// /// * [String] slug: - /// - /// * [String] token: - /// Access token - Future getMySharedLink({ String? key, String? password, String? slug, String? token, }) async { - final response = await getMySharedLinkWithHttpInfo( key: key, password: password, slug: slug, token: token, ); + Future getMySharedLink({ String? key, String? slug, }) async { + final response = await getMySharedLinkWithHttpInfo( key: key, slug: slug, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/sync_api.dart b/mobile/openapi/lib/api/sync_api.dart index 6194fd0f89..e7bc822ace 100644 --- a/mobile/openapi/lib/api/sync_api.dart +++ b/mobile/openapi/lib/api/sync_api.dart @@ -64,121 +64,6 @@ class SyncApi { } } - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSyncWithHttpInfo(AssetDeltaSyncDto assetDeltaSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/delta-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetDeltaSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSync(AssetDeltaSyncDto assetDeltaSyncDto,) async { - final response = await getDeltaSyncWithHttpInfo(assetDeltaSyncDto,); - 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), 'AssetDeltaSyncResponseDto',) as AssetDeltaSyncResponseDto; - - } - return null; - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future getFullSyncForUserWithHttpInfo(AssetFullSyncDto assetFullSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/full-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetFullSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future?> getFullSyncForUser(AssetFullSyncDto assetFullSyncDto,) async { - final response = await getFullSyncForUserWithHttpInfo(assetFullSyncDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Retrieve acknowledgements /// /// Retrieve the synchronization acknowledgments for the current session. diff --git a/mobile/openapi/lib/api/timeline_api.dart b/mobile/openapi/lib/api/timeline_api.dart index f82c362ff7..30a4c123f1 100644 --- a/mobile/openapi/lib/api/timeline_api.dart +++ b/mobile/openapi/lib/api/timeline_api.dart @@ -25,7 +25,7 @@ class TimelineApi { /// Parameters: /// /// * [String] timeBucket (required): - /// Time bucket identifier in YYYY-MM-DD format (e.g., \"2024-01-01\" for January 2024) + /// Time bucket identifier in YYYY-MM-DD format /// /// * [String] albumId: /// Filter assets belonging to a specific album @@ -142,7 +142,7 @@ class TimelineApi { /// Parameters: /// /// * [String] timeBucket (required): - /// Time bucket identifier in YYYY-MM-DD format (e.g., \"2024-01-01\" for January 2024) + /// Time bucket identifier in YYYY-MM-DD format /// /// * [String] albumId: /// Filter assets belonging to a specific album diff --git a/mobile/openapi/lib/api/users_admin_api.dart b/mobile/openapi/lib/api/users_admin_api.dart index 59a4b60096..5e165ffd5d 100644 --- a/mobile/openapi/lib/api/users_admin_api.dart +++ b/mobile/openapi/lib/api/users_admin_api.dart @@ -324,7 +324,6 @@ class UsersAdminApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getUserStatisticsAdminWithHttpInfo(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}/statistics' @@ -376,7 +375,6 @@ class UsersAdminApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getUserStatisticsAdmin(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { final response = await getUserStatisticsAdminWithHttpInfo(id, isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/users_api.dart b/mobile/openapi/lib/api/users_api.dart index 7ccae02c76..401cf4e94b 100644 --- a/mobile/openapi/lib/api/users_api.dart +++ b/mobile/openapi/lib/api/users_api.dart @@ -447,7 +447,7 @@ class UsersApi { /// Retrieve user product key /// /// Retrieve information about whether the current user has a registered product key. - Future getUserLicense() async { + Future getUserLicense() async { final response = await getUserLicenseWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -456,7 +456,7 @@ class UsersApi { // 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), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; @@ -602,7 +602,7 @@ class UsersApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setUserLicense(LicenseKeyDto licenseKeyDto,) async { + Future setUserLicense(LicenseKeyDto licenseKeyDto,) async { final response = await setUserLicenseWithHttpInfo(licenseKeyDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -611,7 +611,7 @@ class UsersApi { // 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), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; @@ -731,7 +731,7 @@ class UsersApi { /// Update current user /// - /// Update the current user making teh API request. + /// Update the current user making the API request. /// /// Note: This method returns the HTTP [Response]. /// @@ -765,7 +765,7 @@ class UsersApi { /// Update current user /// - /// Update the current user making teh API request. + /// Update the current user making the API request. /// /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index bfe469e7c0..b8799a7be5 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -182,14 +182,6 @@ class ApiClient { return valueString == 'true' || valueString == '1'; case 'DateTime': return value is DateTime ? value : DateTime.tryParse(value); - case 'APIKeyCreateDto': - return APIKeyCreateDto.fromJson(value); - case 'APIKeyCreateResponseDto': - return APIKeyCreateResponseDto.fromJson(value); - case 'APIKeyResponseDto': - return APIKeyResponseDto.fromJson(value); - case 'APIKeyUpdateDto': - return APIKeyUpdateDto.fromJson(value); case 'ActivityCreateDto': return ActivityCreateDto.fromJson(value); case 'ActivityResponseDto': @@ -220,6 +212,14 @@ class ApiClient { return AlbumsResponse.fromJson(value); case 'AlbumsUpdate': return AlbumsUpdate.fromJson(value); + case 'ApiKeyCreateDto': + return ApiKeyCreateDto.fromJson(value); + case 'ApiKeyCreateResponseDto': + return ApiKeyCreateResponseDto.fromJson(value); + case 'ApiKeyResponseDto': + return ApiKeyResponseDto.fromJson(value); + case 'ApiKeyUpdateDto': + return ApiKeyUpdateDto.fromJson(value); case 'AssetBulkDeleteDto': return AssetBulkDeleteDto.fromJson(value); case 'AssetBulkUpdateDto': @@ -234,10 +234,6 @@ class ApiClient { return AssetBulkUploadCheckResult.fromJson(value); case 'AssetCopyDto': return AssetCopyDto.fromJson(value); - case 'AssetDeltaSyncDto': - return AssetDeltaSyncDto.fromJson(value); - case 'AssetDeltaSyncResponseDto': - return AssetDeltaSyncResponseDto.fromJson(value); case 'AssetEditAction': return AssetEditActionTypeTransformer().decode(value); case 'AssetEditActionItemDto': @@ -262,8 +258,8 @@ class ApiClient { return AssetFaceUpdateItem.fromJson(value); case 'AssetFaceWithoutPersonResponseDto': return AssetFaceWithoutPersonResponseDto.fromJson(value); - case 'AssetFullSyncDto': - return AssetFullSyncDto.fromJson(value); + case 'AssetIdErrorReason': + return AssetIdErrorReasonTypeTransformer().decode(value); case 'AssetIdsDto': return AssetIdsDto.fromJson(value); case 'AssetIdsResponseDto': @@ -298,6 +294,8 @@ class ApiClient { return AssetOcrResponseDto.fromJson(value); case 'AssetOrder': return AssetOrderTypeTransformer().decode(value); + case 'AssetRejectReason': + return AssetRejectReasonTypeTransformer().decode(value); case 'AssetResponseDto': return AssetResponseDto.fromJson(value); case 'AssetStackResponseDto': @@ -306,6 +304,8 @@ class ApiClient { return AssetStatsResponseDto.fromJson(value); case 'AssetTypeEnum': return AssetTypeEnumTypeTransformer().decode(value); + case 'AssetUploadAction': + return AssetUploadActionTypeTransformer().decode(value); case 'AssetVisibility': return AssetVisibilityTypeTransformer().decode(value); case 'AudioCodec': @@ -330,10 +330,6 @@ class ApiClient { return CastUpdate.fromJson(value); case 'ChangePasswordDto': return ChangePasswordDto.fromJson(value); - case 'CheckExistingAssetsDto': - return CheckExistingAssetsDto.fromJson(value); - case 'CheckExistingAssetsResponseDto': - return CheckExistingAssetsResponseDto.fromJson(value); case 'Colorspace': return ColorspaceTypeTransformer().decode(value); case 'ContributorCountResponseDto': @@ -368,6 +364,10 @@ class ApiClient { return DownloadUpdate.fromJson(value); case 'DuplicateDetectionConfig': return DuplicateDetectionConfig.fromJson(value); + case 'DuplicateResolveDto': + return DuplicateResolveDto.fromJson(value); + case 'DuplicateResolveGroupDto': + return DuplicateResolveGroupDto.fromJson(value); case 'DuplicateResponseDto': return DuplicateResponseDto.fromJson(value); case 'EmailNotificationsResponse': @@ -398,8 +398,6 @@ class ApiClient { return LibraryStatsResponseDto.fromJson(value); case 'LicenseKeyDto': return LicenseKeyDto.fromJson(value); - case 'LicenseResponseDto': - return LicenseResponseDto.fromJson(value); case 'LogLevel': return LogLevelTypeTransformer().decode(value); case 'LoginCredentialDto': @@ -526,6 +524,14 @@ class ApiClient { return PluginContextTypeTypeTransformer().decode(value); case 'PluginFilterResponseDto': return PluginFilterResponseDto.fromJson(value); + case 'PluginJsonSchema': + return PluginJsonSchema.fromJson(value); + case 'PluginJsonSchemaProperty': + return PluginJsonSchemaProperty.fromJson(value); + case 'PluginJsonSchemaPropertyAdditionalProperties': + return PluginJsonSchemaPropertyAdditionalProperties.fromJson(value); + case 'PluginJsonSchemaType': + return PluginJsonSchemaTypeTypeTransformer().decode(value); case 'PluginResponseDto': return PluginResponseDto.fromJson(value); case 'PluginTriggerResponseDto': @@ -608,8 +614,6 @@ class ApiClient { return ServerStatsResponseDto.fromJson(value); case 'ServerStorageResponseDto': return ServerStorageResponseDto.fromJson(value); - case 'ServerThemeDto': - return ServerThemeDto.fromJson(value); case 'ServerVersionHistoryResponseDto': return ServerVersionHistoryResponseDto.fromJson(value); case 'ServerVersionResponseDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 830325a5b6..3b36b23d6c 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -61,6 +61,9 @@ String parameterToString(dynamic value) { if (value is AssetEditAction) { return AssetEditActionTypeTransformer().encode(value).toString(); } + if (value is AssetIdErrorReason) { + return AssetIdErrorReasonTypeTransformer().encode(value).toString(); + } if (value is AssetJobName) { return AssetJobNameTypeTransformer().encode(value).toString(); } @@ -73,9 +76,15 @@ String parameterToString(dynamic value) { if (value is AssetOrder) { return AssetOrderTypeTransformer().encode(value).toString(); } + if (value is AssetRejectReason) { + return AssetRejectReasonTypeTransformer().encode(value).toString(); + } if (value is AssetTypeEnum) { return AssetTypeEnumTypeTransformer().encode(value).toString(); } + if (value is AssetUploadAction) { + return AssetUploadActionTypeTransformer().encode(value).toString(); + } if (value is AssetVisibility) { return AssetVisibilityTypeTransformer().encode(value).toString(); } @@ -133,6 +142,9 @@ String parameterToString(dynamic value) { if (value is PluginContextType) { return PluginContextTypeTypeTransformer().encode(value).toString(); } + if (value is PluginJsonSchemaType) { + return PluginJsonSchemaTypeTypeTransformer().encode(value).toString(); + } if (value is PluginTriggerType) { return PluginTriggerTypeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/activity_create_dto.dart b/mobile/openapi/lib/model/activity_create_dto.dart index fb4b6d084e..bc220e64ce 100644 --- a/mobile/openapi/lib/model/activity_create_dto.dart +++ b/mobile/openapi/lib/model/activity_create_dto.dart @@ -40,7 +40,6 @@ class ActivityCreateDto { /// String? comment; - /// Activity type (like or comment) ReactionType type; @override diff --git a/mobile/openapi/lib/model/activity_response_dto.dart b/mobile/openapi/lib/model/activity_response_dto.dart index dadb45d8ac..1b0e279ab7 100644 --- a/mobile/openapi/lib/model/activity_response_dto.dart +++ b/mobile/openapi/lib/model/activity_response_dto.dart @@ -33,7 +33,6 @@ class ActivityResponseDto { /// Activity ID String id; - /// Activity type ReactionType type; UserResponseDto user; @@ -72,7 +71,9 @@ class ActivityResponseDto { } else { // json[r'comment'] = null; } - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'type'] = this.type; json[r'user'] = this.user; @@ -90,7 +91,7 @@ class ActivityResponseDto { return ActivityResponseDto( assetId: mapValueOfType(json, r'assetId'), comment: mapValueOfType(json, r'comment'), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, id: mapValueOfType(json, r'id')!, type: ReactionType.fromJson(json[r'type'])!, user: UserResponseDto.fromJson(json[r'user'])!, diff --git a/mobile/openapi/lib/model/activity_statistics_response_dto.dart b/mobile/openapi/lib/model/activity_statistics_response_dto.dart index 15ad2a170e..d9ac019ee2 100644 --- a/mobile/openapi/lib/model/activity_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/activity_statistics_response_dto.dart @@ -18,9 +18,15 @@ class ActivityStatisticsResponseDto { }); /// Number of comments + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int comments; /// Number of likes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int likes; @override diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 43e686fbdc..348e25ddaf 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -17,7 +17,6 @@ class AlbumResponseDto { required this.albumThumbnailAssetId, this.albumUsers = const [], required this.assetCount, - this.assets = const [], this.contributorCounts = const [], required this.createdAt, required this.description, @@ -43,10 +42,11 @@ class AlbumResponseDto { List albumUsers; /// Number of assets + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int assetCount; - List assets; - List contributorCounts; /// Creation date @@ -82,7 +82,6 @@ class AlbumResponseDto { /// DateTime? lastModifiedAssetTimestamp; - /// Asset sort order /// /// 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 @@ -117,7 +116,6 @@ class AlbumResponseDto { other.albumThumbnailAssetId == albumThumbnailAssetId && _deepEquality.equals(other.albumUsers, albumUsers) && other.assetCount == assetCount && - _deepEquality.equals(other.assets, assets) && _deepEquality.equals(other.contributorCounts, contributorCounts) && other.createdAt == createdAt && other.description == description && @@ -140,7 +138,6 @@ class AlbumResponseDto { (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) + (albumUsers.hashCode) + (assetCount.hashCode) + - (assets.hashCode) + (contributorCounts.hashCode) + (createdAt.hashCode) + (description.hashCode) + @@ -157,7 +154,7 @@ class AlbumResponseDto { (updatedAt.hashCode); @override - String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, assets=$assets, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]'; + String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -169,7 +166,6 @@ class AlbumResponseDto { } json[r'albumUsers'] = this.albumUsers; json[r'assetCount'] = this.assetCount; - json[r'assets'] = this.assets; json[r'contributorCounts'] = this.contributorCounts; json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'description'] = this.description; @@ -216,7 +212,6 @@ class AlbumResponseDto { albumThumbnailAssetId: mapValueOfType(json, r'albumThumbnailAssetId'), albumUsers: AlbumUserResponseDto.listFromJson(json[r'albumUsers']), assetCount: mapValueOfType(json, r'assetCount')!, - assets: AssetResponseDto.listFromJson(json[r'assets']), contributorCounts: ContributorCountResponseDto.listFromJson(json[r'contributorCounts']), createdAt: mapDateTime(json, r'createdAt', r'')!, description: mapValueOfType(json, r'description')!, @@ -282,7 +277,6 @@ class AlbumResponseDto { 'albumThumbnailAssetId', 'albumUsers', 'assetCount', - 'assets', 'createdAt', 'description', 'hasSharedLink', diff --git a/mobile/openapi/lib/model/album_statistics_response_dto.dart b/mobile/openapi/lib/model/album_statistics_response_dto.dart index 127334e687..0f440d572d 100644 --- a/mobile/openapi/lib/model/album_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/album_statistics_response_dto.dart @@ -19,12 +19,21 @@ class AlbumStatisticsResponseDto { }); /// Number of non-shared albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int notShared; /// Number of owned albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int owned; /// Number of shared albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int shared; @override diff --git a/mobile/openapi/lib/model/album_user_add_dto.dart b/mobile/openapi/lib/model/album_user_add_dto.dart index c448a0b4b7..ee457905bd 100644 --- a/mobile/openapi/lib/model/album_user_add_dto.dart +++ b/mobile/openapi/lib/model/album_user_add_dto.dart @@ -13,12 +13,17 @@ part of openapi.api; class AlbumUserAddDto { /// Returns a new [AlbumUserAddDto] instance. AlbumUserAddDto({ - this.role = AlbumUserRole.editor, + this.role, required this.userId, }); - /// Album user role - AlbumUserRole role; + /// + /// 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. + /// + AlbumUserRole? role; /// User ID String userId; @@ -31,7 +36,7 @@ class AlbumUserAddDto { @override int get hashCode => // ignore: unnecessary_parenthesis - (role.hashCode) + + (role == null ? 0 : role!.hashCode) + (userId.hashCode); @override @@ -39,7 +44,11 @@ class AlbumUserAddDto { Map toJson() { final json = {}; + if (this.role != null) { json[r'role'] = this.role; + } else { + // json[r'role'] = null; + } json[r'userId'] = this.userId; return json; } @@ -53,7 +62,7 @@ class AlbumUserAddDto { final json = value.cast(); return AlbumUserAddDto( - role: AlbumUserRole.fromJson(json[r'role']) ?? AlbumUserRole.editor, + role: AlbumUserRole.fromJson(json[r'role']), userId: mapValueOfType(json, r'userId')!, ); } diff --git a/mobile/openapi/lib/model/album_user_create_dto.dart b/mobile/openapi/lib/model/album_user_create_dto.dart index 8006748341..26aa35ae78 100644 --- a/mobile/openapi/lib/model/album_user_create_dto.dart +++ b/mobile/openapi/lib/model/album_user_create_dto.dart @@ -17,7 +17,6 @@ class AlbumUserCreateDto { required this.userId, }); - /// Album user role AlbumUserRole role; /// User ID diff --git a/mobile/openapi/lib/model/album_user_response_dto.dart b/mobile/openapi/lib/model/album_user_response_dto.dart index 8d0c01cfb8..bbae03fba7 100644 --- a/mobile/openapi/lib/model/album_user_response_dto.dart +++ b/mobile/openapi/lib/model/album_user_response_dto.dart @@ -17,7 +17,6 @@ class AlbumUserResponseDto { required this.user, }); - /// Album user role AlbumUserRole role; UserResponseDto user; diff --git a/mobile/openapi/lib/model/albums_add_assets_response_dto.dart b/mobile/openapi/lib/model/albums_add_assets_response_dto.dart index 743a9f0645..99e679222e 100644 --- a/mobile/openapi/lib/model/albums_add_assets_response_dto.dart +++ b/mobile/openapi/lib/model/albums_add_assets_response_dto.dart @@ -17,7 +17,6 @@ class AlbumsAddAssetsResponseDto { required this.success, }); - /// Error reason /// /// 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 diff --git a/mobile/openapi/lib/model/albums_response.dart b/mobile/openapi/lib/model/albums_response.dart index 520ee171c1..def205de90 100644 --- a/mobile/openapi/lib/model/albums_response.dart +++ b/mobile/openapi/lib/model/albums_response.dart @@ -13,10 +13,9 @@ part of openapi.api; class AlbumsResponse { /// Returns a new [AlbumsResponse] instance. AlbumsResponse({ - this.defaultAssetOrder = AssetOrder.desc, + required this.defaultAssetOrder, }); - /// Default asset order for albums AssetOrder defaultAssetOrder; @override diff --git a/mobile/openapi/lib/model/albums_update.dart b/mobile/openapi/lib/model/albums_update.dart index 107c65dd1e..d61b5c1398 100644 --- a/mobile/openapi/lib/model/albums_update.dart +++ b/mobile/openapi/lib/model/albums_update.dart @@ -16,7 +16,6 @@ class AlbumsUpdate { this.defaultAssetOrder, }); - /// Default asset order for albums /// /// 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 diff --git a/mobile/openapi/lib/model/api_key_create_dto.dart b/mobile/openapi/lib/model/api_key_create_dto.dart index e64b127820..6d3ffc1eb1 100644 --- a/mobile/openapi/lib/model/api_key_create_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class APIKeyCreateDto { - /// Returns a new [APIKeyCreateDto] instance. - APIKeyCreateDto({ +class ApiKeyCreateDto { + /// Returns a new [ApiKeyCreateDto] instance. + ApiKeyCreateDto({ this.name, this.permissions = const [], }); @@ -30,7 +30,7 @@ class APIKeyCreateDto { List permissions; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyCreateDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyCreateDto && other.name == name && _deepEquality.equals(other.permissions, permissions); @@ -41,7 +41,7 @@ class APIKeyCreateDto { (permissions.hashCode); @override - String toString() => 'APIKeyCreateDto[name=$name, permissions=$permissions]'; + String toString() => 'ApiKeyCreateDto[name=$name, permissions=$permissions]'; Map toJson() { final json = {}; @@ -54,15 +54,15 @@ class APIKeyCreateDto { return json; } - /// Returns a new [APIKeyCreateDto] instance and imports its values from + /// Returns a new [ApiKeyCreateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyCreateDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyCreateDto"); + static ApiKeyCreateDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyCreateDto"); if (value is Map) { final json = value.cast(); - return APIKeyCreateDto( + return ApiKeyCreateDto( name: mapValueOfType(json, r'name'), permissions: Permission.listFromJson(json[r'permissions']), ); @@ -70,11 +70,11 @@ class APIKeyCreateDto { 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 = APIKeyCreateDto.fromJson(row); + final value = ApiKeyCreateDto.fromJson(row); if (value != null) { result.add(value); } @@ -83,12 +83,12 @@ class APIKeyCreateDto { 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 = APIKeyCreateDto.fromJson(entry.value); + final value = ApiKeyCreateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -97,14 +97,14 @@ class APIKeyCreateDto { return map; } - // maps a json object with a list of APIKeyCreateDto-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 ApiKeyCreateDto-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] = APIKeyCreateDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyCreateDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/api_key_create_response_dto.dart b/mobile/openapi/lib/model/api_key_create_response_dto.dart index 7540c4bb26..77b19ebfd2 100644 --- a/mobile/openapi/lib/model/api_key_create_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_response_dto.dart @@ -10,20 +10,20 @@ part of openapi.api; -class APIKeyCreateResponseDto { - /// Returns a new [APIKeyCreateResponseDto] instance. - APIKeyCreateResponseDto({ +class ApiKeyCreateResponseDto { + /// Returns a new [ApiKeyCreateResponseDto] instance. + ApiKeyCreateResponseDto({ required this.apiKey, required this.secret, }); - APIKeyResponseDto apiKey; + ApiKeyResponseDto apiKey; /// API key secret (only shown once) String secret; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyCreateResponseDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyCreateResponseDto && other.apiKey == apiKey && other.secret == secret; @@ -34,7 +34,7 @@ class APIKeyCreateResponseDto { (secret.hashCode); @override - String toString() => 'APIKeyCreateResponseDto[apiKey=$apiKey, secret=$secret]'; + String toString() => 'ApiKeyCreateResponseDto[apiKey=$apiKey, secret=$secret]'; Map toJson() { final json = {}; @@ -43,27 +43,27 @@ class APIKeyCreateResponseDto { return json; } - /// Returns a new [APIKeyCreateResponseDto] instance and imports its values from + /// Returns a new [ApiKeyCreateResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyCreateResponseDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyCreateResponseDto"); + static ApiKeyCreateResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyCreateResponseDto"); if (value is Map) { final json = value.cast(); - return APIKeyCreateResponseDto( - apiKey: APIKeyResponseDto.fromJson(json[r'apiKey'])!, + return ApiKeyCreateResponseDto( + apiKey: ApiKeyResponseDto.fromJson(json[r'apiKey'])!, secret: mapValueOfType(json, r'secret')!, ); } 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 = APIKeyCreateResponseDto.fromJson(row); + final value = ApiKeyCreateResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -72,12 +72,12 @@ class APIKeyCreateResponseDto { 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 = APIKeyCreateResponseDto.fromJson(entry.value); + final value = ApiKeyCreateResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -86,14 +86,14 @@ class APIKeyCreateResponseDto { return map; } - // maps a json object with a list of APIKeyCreateResponseDto-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 ApiKeyCreateResponseDto-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] = APIKeyCreateResponseDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyCreateResponseDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/api_key_response_dto.dart b/mobile/openapi/lib/model/api_key_response_dto.dart index 32ba543342..79099188a3 100644 --- a/mobile/openapi/lib/model/api_key_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_response_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class APIKeyResponseDto { - /// Returns a new [APIKeyResponseDto] instance. - APIKeyResponseDto({ +class ApiKeyResponseDto { + /// Returns a new [ApiKeyResponseDto] instance. + ApiKeyResponseDto({ required this.createdAt, required this.id, required this.name, @@ -36,7 +36,7 @@ class APIKeyResponseDto { DateTime updatedAt; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyResponseDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyResponseDto && other.createdAt == createdAt && other.id == id && other.name == name && @@ -53,42 +53,46 @@ class APIKeyResponseDto { (updatedAt.hashCode); @override - String toString() => 'APIKeyResponseDto[createdAt=$createdAt, id=$id, name=$name, permissions=$permissions, updatedAt=$updatedAt]'; + String toString() => 'ApiKeyResponseDto[createdAt=$createdAt, id=$id, name=$name, permissions=$permissions, updatedAt=$updatedAt]'; Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'name'] = this.name; json[r'permissions'] = this.permissions; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } - /// Returns a new [APIKeyResponseDto] instance and imports its values from + /// Returns a new [ApiKeyResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyResponseDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyResponseDto"); + static ApiKeyResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyResponseDto"); if (value is Map) { final json = value.cast(); - return APIKeyResponseDto( - createdAt: mapDateTime(json, r'createdAt', r'')!, + return ApiKeyResponseDto( + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name')!, permissions: Permission.listFromJson(json[r'permissions']), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } 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 = APIKeyResponseDto.fromJson(row); + final value = ApiKeyResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -97,12 +101,12 @@ class APIKeyResponseDto { 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 = APIKeyResponseDto.fromJson(entry.value); + final value = ApiKeyResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -111,14 +115,14 @@ class APIKeyResponseDto { return map; } - // maps a json object with a list of APIKeyResponseDto-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 ApiKeyResponseDto-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] = APIKeyResponseDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyResponseDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/api_key_update_dto.dart b/mobile/openapi/lib/model/api_key_update_dto.dart index ba107bcda2..c8df4be654 100644 --- a/mobile/openapi/lib/model/api_key_update_dto.dart +++ b/mobile/openapi/lib/model/api_key_update_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class APIKeyUpdateDto { - /// Returns a new [APIKeyUpdateDto] instance. - APIKeyUpdateDto({ +class ApiKeyUpdateDto { + /// Returns a new [ApiKeyUpdateDto] instance. + ApiKeyUpdateDto({ this.name, this.permissions = const [], }); @@ -30,7 +30,7 @@ class APIKeyUpdateDto { List permissions; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyUpdateDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyUpdateDto && other.name == name && _deepEquality.equals(other.permissions, permissions); @@ -41,7 +41,7 @@ class APIKeyUpdateDto { (permissions.hashCode); @override - String toString() => 'APIKeyUpdateDto[name=$name, permissions=$permissions]'; + String toString() => 'ApiKeyUpdateDto[name=$name, permissions=$permissions]'; Map toJson() { final json = {}; @@ -54,15 +54,15 @@ class APIKeyUpdateDto { return json; } - /// Returns a new [APIKeyUpdateDto] instance and imports its values from + /// Returns a new [ApiKeyUpdateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyUpdateDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyUpdateDto"); + static ApiKeyUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyUpdateDto"); if (value is Map) { final json = value.cast(); - return APIKeyUpdateDto( + return ApiKeyUpdateDto( name: mapValueOfType(json, r'name'), permissions: Permission.listFromJson(json[r'permissions']), ); @@ -70,11 +70,11 @@ class APIKeyUpdateDto { 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 = APIKeyUpdateDto.fromJson(row); + final value = ApiKeyUpdateDto.fromJson(row); if (value != null) { result.add(value); } @@ -83,12 +83,12 @@ class APIKeyUpdateDto { 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 = APIKeyUpdateDto.fromJson(entry.value); + final value = ApiKeyUpdateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -97,14 +97,14 @@ class APIKeyUpdateDto { return map; } - // maps a json object with a list of APIKeyUpdateDto-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 ApiKeyUpdateDto-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] = APIKeyUpdateDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyUpdateDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart index 99bac7abfa..f97300b19f 100644 --- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart @@ -70,6 +70,9 @@ class AssetBulkUpdateDto { /// Latitude coordinate /// + /// Minimum value: -90 + /// Maximum value: 90 + /// /// 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. @@ -79,6 +82,9 @@ class AssetBulkUpdateDto { /// Longitude coordinate /// + /// Minimum value: -180 + /// Maximum value: 180 + /// /// 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. @@ -90,7 +96,7 @@ class AssetBulkUpdateDto { /// /// Minimum value: -1 /// Maximum value: 5 - num? rating; + int? rating; /// Time zone (IANA timezone) /// @@ -101,7 +107,6 @@ class AssetBulkUpdateDto { /// String? timeZone; - /// Asset visibility /// /// 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 @@ -217,9 +222,7 @@ class AssetBulkUpdateDto { isFavorite: mapValueOfType(json, r'isFavorite'), latitude: num.parse('${json[r'latitude']}'), longitude: num.parse('${json[r'longitude']}'), - rating: json[r'rating'] == null - ? null - : num.parse('${json[r'rating']}'), + rating: mapValueOfType(json, r'rating'), timeZone: mapValueOfType(json, r'timeZone'), visibility: AssetVisibility.fromJson(json[r'visibility']), ); diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart index b56370f689..bf3ee8e244 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart @@ -20,8 +20,7 @@ class AssetBulkUploadCheckResult { this.reason, }); - /// Upload action - AssetBulkUploadCheckResultActionEnum action; + AssetUploadAction action; /// Existing asset ID if duplicate /// @@ -44,8 +43,13 @@ class AssetBulkUploadCheckResult { /// bool? isTrashed; - /// Rejection reason if rejected - AssetBulkUploadCheckResultReasonEnum? reason; + /// + /// 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. + /// + AssetRejectReason? reason; @override bool operator ==(Object other) => identical(this, other) || other is AssetBulkUploadCheckResult && @@ -98,11 +102,11 @@ class AssetBulkUploadCheckResult { final json = value.cast(); return AssetBulkUploadCheckResult( - action: AssetBulkUploadCheckResultActionEnum.fromJson(json[r'action'])!, + action: AssetUploadAction.fromJson(json[r'action'])!, assetId: mapValueOfType(json, r'assetId'), id: mapValueOfType(json, r'id')!, isTrashed: mapValueOfType(json, r'isTrashed'), - reason: AssetBulkUploadCheckResultReasonEnum.fromJson(json[r'reason']), + reason: AssetRejectReason.fromJson(json[r'reason']), ); } return null; @@ -155,151 +159,3 @@ class AssetBulkUploadCheckResult { }; } -/// Upload action -class AssetBulkUploadCheckResultActionEnum { - /// Instantiate a new enum with the provided [value]. - const AssetBulkUploadCheckResultActionEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const accept = AssetBulkUploadCheckResultActionEnum._(r'accept'); - static const reject = AssetBulkUploadCheckResultActionEnum._(r'reject'); - - /// List of all possible values in this [enum][AssetBulkUploadCheckResultActionEnum]. - static const values = [ - accept, - reject, - ]; - - static AssetBulkUploadCheckResultActionEnum? fromJson(dynamic value) => AssetBulkUploadCheckResultActionEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetBulkUploadCheckResultActionEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [AssetBulkUploadCheckResultActionEnum] to String, -/// and [decode] dynamic data back to [AssetBulkUploadCheckResultActionEnum]. -class AssetBulkUploadCheckResultActionEnumTypeTransformer { - factory AssetBulkUploadCheckResultActionEnumTypeTransformer() => _instance ??= const AssetBulkUploadCheckResultActionEnumTypeTransformer._(); - - const AssetBulkUploadCheckResultActionEnumTypeTransformer._(); - - String encode(AssetBulkUploadCheckResultActionEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a AssetBulkUploadCheckResultActionEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - AssetBulkUploadCheckResultActionEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'accept': return AssetBulkUploadCheckResultActionEnum.accept; - case r'reject': return AssetBulkUploadCheckResultActionEnum.reject; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [AssetBulkUploadCheckResultActionEnumTypeTransformer] instance. - static AssetBulkUploadCheckResultActionEnumTypeTransformer? _instance; -} - - -/// Rejection reason if rejected -class AssetBulkUploadCheckResultReasonEnum { - /// Instantiate a new enum with the provided [value]. - const AssetBulkUploadCheckResultReasonEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const duplicate = AssetBulkUploadCheckResultReasonEnum._(r'duplicate'); - static const unsupportedFormat = AssetBulkUploadCheckResultReasonEnum._(r'unsupported-format'); - - /// List of all possible values in this [enum][AssetBulkUploadCheckResultReasonEnum]. - static const values = [ - duplicate, - unsupportedFormat, - ]; - - static AssetBulkUploadCheckResultReasonEnum? fromJson(dynamic value) => AssetBulkUploadCheckResultReasonEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetBulkUploadCheckResultReasonEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [AssetBulkUploadCheckResultReasonEnum] to String, -/// and [decode] dynamic data back to [AssetBulkUploadCheckResultReasonEnum]. -class AssetBulkUploadCheckResultReasonEnumTypeTransformer { - factory AssetBulkUploadCheckResultReasonEnumTypeTransformer() => _instance ??= const AssetBulkUploadCheckResultReasonEnumTypeTransformer._(); - - const AssetBulkUploadCheckResultReasonEnumTypeTransformer._(); - - String encode(AssetBulkUploadCheckResultReasonEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a AssetBulkUploadCheckResultReasonEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - AssetBulkUploadCheckResultReasonEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'duplicate': return AssetBulkUploadCheckResultReasonEnum.duplicate; - case r'unsupported-format': return AssetBulkUploadCheckResultReasonEnum.unsupportedFormat; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [AssetBulkUploadCheckResultReasonEnumTypeTransformer] instance. - static AssetBulkUploadCheckResultReasonEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/asset_delta_sync_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_dto.dart deleted file mode 100644 index 22c09752d2..0000000000 --- a/mobile/openapi/lib/model/asset_delta_sync_dto.dart +++ /dev/null @@ -1,111 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class AssetDeltaSyncDto { - /// Returns a new [AssetDeltaSyncDto] instance. - AssetDeltaSyncDto({ - required this.updatedAfter, - this.userIds = const [], - }); - - /// Sync assets updated after this date - DateTime updatedAfter; - - /// User IDs to sync - List userIds; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetDeltaSyncDto && - other.updatedAfter == updatedAfter && - _deepEquality.equals(other.userIds, userIds); - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (updatedAfter.hashCode) + - (userIds.hashCode); - - @override - String toString() => 'AssetDeltaSyncDto[updatedAfter=$updatedAfter, userIds=$userIds]'; - - Map toJson() { - final json = {}; - json[r'updatedAfter'] = this.updatedAfter.toUtc().toIso8601String(); - json[r'userIds'] = this.userIds; - return json; - } - - /// Returns a new [AssetDeltaSyncDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetDeltaSyncDto? fromJson(dynamic value) { - upgradeDto(value, "AssetDeltaSyncDto"); - if (value is Map) { - final json = value.cast(); - - return AssetDeltaSyncDto( - updatedAfter: mapDateTime(json, r'updatedAfter', r'')!, - userIds: json[r'userIds'] is Iterable - ? (json[r'userIds'] as Iterable).cast().toList(growable: false) - : const [], - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetDeltaSyncDto.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 = AssetDeltaSyncDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetDeltaSyncDto-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] = AssetDeltaSyncDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'updatedAfter', - 'userIds', - }; -} - diff --git a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart deleted file mode 100644 index 7351840b11..0000000000 --- a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart +++ /dev/null @@ -1,120 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class AssetDeltaSyncResponseDto { - /// Returns a new [AssetDeltaSyncResponseDto] instance. - AssetDeltaSyncResponseDto({ - this.deleted = const [], - required this.needsFullSync, - this.upserted = const [], - }); - - /// Deleted asset IDs - List deleted; - - /// Whether full sync is needed - bool needsFullSync; - - /// Upserted assets - List upserted; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetDeltaSyncResponseDto && - _deepEquality.equals(other.deleted, deleted) && - other.needsFullSync == needsFullSync && - _deepEquality.equals(other.upserted, upserted); - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (deleted.hashCode) + - (needsFullSync.hashCode) + - (upserted.hashCode); - - @override - String toString() => 'AssetDeltaSyncResponseDto[deleted=$deleted, needsFullSync=$needsFullSync, upserted=$upserted]'; - - Map toJson() { - final json = {}; - json[r'deleted'] = this.deleted; - json[r'needsFullSync'] = this.needsFullSync; - json[r'upserted'] = this.upserted; - return json; - } - - /// Returns a new [AssetDeltaSyncResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetDeltaSyncResponseDto? fromJson(dynamic value) { - upgradeDto(value, "AssetDeltaSyncResponseDto"); - if (value is Map) { - final json = value.cast(); - - return AssetDeltaSyncResponseDto( - deleted: json[r'deleted'] is Iterable - ? (json[r'deleted'] as Iterable).cast().toList(growable: false) - : const [], - needsFullSync: mapValueOfType(json, r'needsFullSync')!, - upserted: AssetResponseDto.listFromJson(json[r'upserted']), - ); - } - 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 = AssetDeltaSyncResponseDto.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 = AssetDeltaSyncResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetDeltaSyncResponseDto-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] = AssetDeltaSyncResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'deleted', - 'needsFullSync', - 'upserted', - }; -} - diff --git a/mobile/openapi/lib/model/asset_edit_action_item_dto.dart b/mobile/openapi/lib/model/asset_edit_action_item_dto.dart index 7829de4bd5..1b19612bf3 100644 --- a/mobile/openapi/lib/model/asset_edit_action_item_dto.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_dto.dart @@ -17,10 +17,9 @@ class AssetEditActionItemDto { required this.parameters, }); - /// Type of edit action to perform AssetEditAction action; - AssetEditActionItemDtoParameters parameters; + Map parameters; @override bool operator ==(Object other) => identical(this, other) || other is AssetEditActionItemDto && @@ -53,7 +52,7 @@ class AssetEditActionItemDto { return AssetEditActionItemDto( action: AssetEditAction.fromJson(json[r'action'])!, - parameters: AssetEditActionItemDtoParameters.fromJson(json[r'parameters'])!, + parameters: json[r'parameters'], ); } return null; diff --git a/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart b/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart index fc67aa022f..2086f72929 100644 --- a/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart @@ -44,7 +44,6 @@ class AssetEditActionItemDtoParameters { /// Rotation angle in degrees num angle; - /// Axis to mirror along MirrorAxis axis; @override diff --git a/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart b/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart index a23a1ef5f3..3315fe8579 100644 --- a/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart @@ -18,9 +18,9 @@ class AssetEditActionItemResponseDto { required this.parameters, }); - /// Type of edit action to perform AssetEditAction action; + /// Asset edit ID String id; AssetEditActionItemDtoParameters parameters; diff --git a/mobile/openapi/lib/model/asset_face_create_dto.dart b/mobile/openapi/lib/model/asset_face_create_dto.dart index 3ecc20c699..29c28175cd 100644 --- a/mobile/openapi/lib/model/asset_face_create_dto.dart +++ b/mobile/openapi/lib/model/asset_face_create_dto.dart @@ -27,24 +27,42 @@ class AssetFaceCreateDto { String assetId; /// Face bounding box height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int height; /// Image height in pixels + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageHeight; /// Image width in pixels + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageWidth; /// Person ID String personId; /// Face bounding box width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int width; /// Face bounding box X coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int x; /// Face bounding box Y coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int y; @override diff --git a/mobile/openapi/lib/model/asset_face_response_dto.dart b/mobile/openapi/lib/model/asset_face_response_dto.dart index 61d972a0c4..21b86dfe4e 100644 --- a/mobile/openapi/lib/model/asset_face_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_response_dto.dart @@ -25,30 +25,46 @@ class AssetFaceResponseDto { }); /// Bounding box X1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; /// Bounding box X2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; /// Bounding box Y1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; /// Bounding box Y2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Face ID String id; /// Image height in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageHeight; /// Image width in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageWidth; - /// Person associated with face PersonResponseDto? person; - /// Face detection source type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart index 1ae5cef07e..4a4a2a658e 100644 --- a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart @@ -24,27 +24,44 @@ class AssetFaceWithoutPersonResponseDto { }); /// Bounding box X1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; /// Bounding box X2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; /// Bounding box Y1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; /// Bounding box Y2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Face ID String id; /// Image height in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageHeight; /// Image width in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageWidth; - /// Face detection source type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/asset_full_sync_dto.dart b/mobile/openapi/lib/model/asset_full_sync_dto.dart deleted file mode 100644 index 3fabb1cac6..0000000000 --- a/mobile/openapi/lib/model/asset_full_sync_dto.dart +++ /dev/null @@ -1,147 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class AssetFullSyncDto { - /// Returns a new [AssetFullSyncDto] instance. - AssetFullSyncDto({ - this.lastId, - required this.limit, - required this.updatedUntil, - this.userId, - }); - - /// Last asset ID (pagination) - /// - /// 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? lastId; - - /// Maximum number of assets to return - /// - /// Minimum value: 1 - int limit; - - /// Sync assets updated until this date - DateTime updatedUntil; - - /// Filter by user ID - /// - /// 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? userId; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetFullSyncDto && - other.lastId == lastId && - other.limit == limit && - other.updatedUntil == updatedUntil && - other.userId == userId; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (lastId == null ? 0 : lastId!.hashCode) + - (limit.hashCode) + - (updatedUntil.hashCode) + - (userId == null ? 0 : userId!.hashCode); - - @override - String toString() => 'AssetFullSyncDto[lastId=$lastId, limit=$limit, updatedUntil=$updatedUntil, userId=$userId]'; - - Map toJson() { - final json = {}; - if (this.lastId != null) { - json[r'lastId'] = this.lastId; - } else { - // json[r'lastId'] = null; - } - json[r'limit'] = this.limit; - json[r'updatedUntil'] = this.updatedUntil.toUtc().toIso8601String(); - if (this.userId != null) { - json[r'userId'] = this.userId; - } else { - // json[r'userId'] = null; - } - return json; - } - - /// Returns a new [AssetFullSyncDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetFullSyncDto? fromJson(dynamic value) { - upgradeDto(value, "AssetFullSyncDto"); - if (value is Map) { - final json = value.cast(); - - return AssetFullSyncDto( - lastId: mapValueOfType(json, r'lastId'), - limit: mapValueOfType(json, r'limit')!, - updatedUntil: mapDateTime(json, r'updatedUntil', r'')!, - userId: mapValueOfType(json, r'userId'), - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetFullSyncDto.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 = AssetFullSyncDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetFullSyncDto-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] = AssetFullSyncDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'limit', - 'updatedUntil', - }; -} - diff --git a/mobile/openapi/lib/model/asset_id_error_reason.dart b/mobile/openapi/lib/model/asset_id_error_reason.dart new file mode 100644 index 0000000000..c51eab1692 --- /dev/null +++ b/mobile/openapi/lib/model/asset_id_error_reason.dart @@ -0,0 +1,88 @@ +// +// 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; + +/// Error reason if failed +class AssetIdErrorReason { + /// Instantiate a new enum with the provided [value]. + const AssetIdErrorReason._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const duplicate = AssetIdErrorReason._(r'duplicate'); + static const noPermission = AssetIdErrorReason._(r'no_permission'); + static const notFound = AssetIdErrorReason._(r'not_found'); + + /// List of all possible values in this [enum][AssetIdErrorReason]. + static const values = [ + duplicate, + noPermission, + notFound, + ]; + + static AssetIdErrorReason? fromJson(dynamic value) => AssetIdErrorReasonTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetIdErrorReason.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetIdErrorReason] to String, +/// and [decode] dynamic data back to [AssetIdErrorReason]. +class AssetIdErrorReasonTypeTransformer { + factory AssetIdErrorReasonTypeTransformer() => _instance ??= const AssetIdErrorReasonTypeTransformer._(); + + const AssetIdErrorReasonTypeTransformer._(); + + String encode(AssetIdErrorReason data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetIdErrorReason. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetIdErrorReason? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'duplicate': return AssetIdErrorReason.duplicate; + case r'no_permission': return AssetIdErrorReason.noPermission; + case r'not_found': return AssetIdErrorReason.notFound; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetIdErrorReasonTypeTransformer] instance. + static AssetIdErrorReasonTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/asset_ids_response_dto.dart b/mobile/openapi/lib/model/asset_ids_response_dto.dart index 9745283021..cafe1b21b9 100644 --- a/mobile/openapi/lib/model/asset_ids_response_dto.dart +++ b/mobile/openapi/lib/model/asset_ids_response_dto.dart @@ -21,8 +21,13 @@ class AssetIdsResponseDto { /// Asset ID String assetId; - /// Error reason if failed - AssetIdsResponseDtoErrorEnum? error; + /// + /// 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. + /// + AssetIdErrorReason? error; /// Whether operation succeeded bool success; @@ -65,7 +70,7 @@ class AssetIdsResponseDto { return AssetIdsResponseDto( assetId: mapValueOfType(json, r'assetId')!, - error: AssetIdsResponseDtoErrorEnum.fromJson(json[r'error']), + error: AssetIdErrorReason.fromJson(json[r'error']), success: mapValueOfType(json, r'success')!, ); } @@ -119,80 +124,3 @@ class AssetIdsResponseDto { }; } -/// Error reason if failed -class AssetIdsResponseDtoErrorEnum { - /// Instantiate a new enum with the provided [value]. - const AssetIdsResponseDtoErrorEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const duplicate = AssetIdsResponseDtoErrorEnum._(r'duplicate'); - static const noPermission = AssetIdsResponseDtoErrorEnum._(r'no_permission'); - static const notFound = AssetIdsResponseDtoErrorEnum._(r'not_found'); - - /// List of all possible values in this [enum][AssetIdsResponseDtoErrorEnum]. - static const values = [ - duplicate, - noPermission, - notFound, - ]; - - static AssetIdsResponseDtoErrorEnum? fromJson(dynamic value) => AssetIdsResponseDtoErrorEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetIdsResponseDtoErrorEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [AssetIdsResponseDtoErrorEnum] to String, -/// and [decode] dynamic data back to [AssetIdsResponseDtoErrorEnum]. -class AssetIdsResponseDtoErrorEnumTypeTransformer { - factory AssetIdsResponseDtoErrorEnumTypeTransformer() => _instance ??= const AssetIdsResponseDtoErrorEnumTypeTransformer._(); - - const AssetIdsResponseDtoErrorEnumTypeTransformer._(); - - String encode(AssetIdsResponseDtoErrorEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a AssetIdsResponseDtoErrorEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - AssetIdsResponseDtoErrorEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'duplicate': return AssetIdsResponseDtoErrorEnum.duplicate; - case r'no_permission': return AssetIdsResponseDtoErrorEnum.noPermission; - case r'not_found': return AssetIdsResponseDtoErrorEnum.notFound; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [AssetIdsResponseDtoErrorEnumTypeTransformer] instance. - static AssetIdsResponseDtoErrorEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/asset_jobs_dto.dart b/mobile/openapi/lib/model/asset_jobs_dto.dart index 0aa5544a3a..5085e3820c 100644 --- a/mobile/openapi/lib/model/asset_jobs_dto.dart +++ b/mobile/openapi/lib/model/asset_jobs_dto.dart @@ -20,7 +20,6 @@ class AssetJobsDto { /// Asset IDs List assetIds; - /// Job name AssetJobName name; @override diff --git a/mobile/openapi/lib/model/asset_media_response_dto.dart b/mobile/openapi/lib/model/asset_media_response_dto.dart index 905e738b6e..6dc5cd3c92 100644 --- a/mobile/openapi/lib/model/asset_media_response_dto.dart +++ b/mobile/openapi/lib/model/asset_media_response_dto.dart @@ -20,7 +20,6 @@ class AssetMediaResponseDto { /// Asset media ID String id; - /// Upload status AssetMediaStatus status; @override diff --git a/mobile/openapi/lib/model/asset_media_size.dart b/mobile/openapi/lib/model/asset_media_size.dart index 087d19da1f..ed7a72a613 100644 --- a/mobile/openapi/lib/model/asset_media_size.dart +++ b/mobile/openapi/lib/model/asset_media_size.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Asset media size class AssetMediaSize { /// Instantiate a new enum with the provided [value]. const AssetMediaSize._(this.value); diff --git a/mobile/openapi/lib/model/asset_media_status.dart b/mobile/openapi/lib/model/asset_media_status.dart index b45918e5c3..44438f15ab 100644 --- a/mobile/openapi/lib/model/asset_media_status.dart +++ b/mobile/openapi/lib/model/asset_media_status.dart @@ -24,13 +24,11 @@ class AssetMediaStatus { String toJson() => value; static const created = AssetMediaStatus._(r'created'); - static const replaced = AssetMediaStatus._(r'replaced'); static const duplicate = AssetMediaStatus._(r'duplicate'); /// List of all possible values in this [enum][AssetMediaStatus]. static const values = [ created, - replaced, duplicate, ]; @@ -71,7 +69,6 @@ class AssetMediaStatusTypeTransformer { if (data != null) { switch (data) { case r'created': return AssetMediaStatus.created; - case r'replaced': return AssetMediaStatus.replaced; case r'duplicate': return AssetMediaStatus.duplicate; default: if (!allowNull) { diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart index b79a693726..3e16ed8721 100644 --- a/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart @@ -16,7 +16,7 @@ class AssetMetadataBulkResponseDto { required this.assetId, required this.key, required this.updatedAt, - required this.value, + this.value = const {}, }); /// Asset ID @@ -29,14 +29,14 @@ class AssetMetadataBulkResponseDto { DateTime updatedAt; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkResponseDto && other.assetId == assetId && other.key == key && other.updatedAt == updatedAt && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -53,7 +53,9 @@ class AssetMetadataBulkResponseDto { final json = {}; json[r'assetId'] = this.assetId; json[r'key'] = this.key; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); json[r'value'] = this.value; return json; } @@ -69,8 +71,8 @@ class AssetMetadataBulkResponseDto { return AssetMetadataBulkResponseDto( assetId: mapValueOfType(json, r'assetId')!, key: mapValueOfType(json, r'key')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, - value: mapValueOfType(json, r'value')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart index caaf379b30..e4eab08bf1 100644 --- a/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart @@ -15,7 +15,7 @@ class AssetMetadataBulkUpsertItemDto { AssetMetadataBulkUpsertItemDto({ required this.assetId, required this.key, - required this.value, + this.value = const {}, }); /// Asset ID @@ -25,13 +25,13 @@ class AssetMetadataBulkUpsertItemDto { String key; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkUpsertItemDto && other.assetId == assetId && other.key == key && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -62,7 +62,7 @@ class AssetMetadataBulkUpsertItemDto { return AssetMetadataBulkUpsertItemDto( assetId: mapValueOfType(json, r'assetId')!, key: mapValueOfType(json, r'key')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_metadata_response_dto.dart b/mobile/openapi/lib/model/asset_metadata_response_dto.dart index 2c3faab178..d3562f5a48 100644 --- a/mobile/openapi/lib/model/asset_metadata_response_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_response_dto.dart @@ -15,7 +15,7 @@ class AssetMetadataResponseDto { AssetMetadataResponseDto({ required this.key, required this.updatedAt, - required this.value, + this.value = const {}, }); /// Metadata key @@ -25,13 +25,13 @@ class AssetMetadataResponseDto { DateTime updatedAt; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataResponseDto && other.key == key && other.updatedAt == updatedAt && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -46,7 +46,9 @@ class AssetMetadataResponseDto { Map toJson() { final json = {}; json[r'key'] = this.key; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); json[r'value'] = this.value; return json; } @@ -61,8 +63,8 @@ class AssetMetadataResponseDto { return AssetMetadataResponseDto( key: mapValueOfType(json, r'key')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, - value: mapValueOfType(json, r'value')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart b/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart index 8a6bcb9b01..70de1941f3 100644 --- a/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart @@ -14,19 +14,19 @@ class AssetMetadataUpsertItemDto { /// Returns a new [AssetMetadataUpsertItemDto] instance. AssetMetadataUpsertItemDto({ required this.key, - required this.value, + this.value = const {}, }); /// Metadata key String key; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataUpsertItemDto && other.key == key && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -54,7 +54,7 @@ class AssetMetadataUpsertItemDto { return AssetMetadataUpsertItemDto( key: mapValueOfType(json, r'key')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_reject_reason.dart b/mobile/openapi/lib/model/asset_reject_reason.dart new file mode 100644 index 0000000000..a31e1e6117 --- /dev/null +++ b/mobile/openapi/lib/model/asset_reject_reason.dart @@ -0,0 +1,85 @@ +// +// 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; + +/// Rejection reason if rejected +class AssetRejectReason { + /// Instantiate a new enum with the provided [value]. + const AssetRejectReason._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const duplicate = AssetRejectReason._(r'duplicate'); + static const unsupportedFormat = AssetRejectReason._(r'unsupported-format'); + + /// List of all possible values in this [enum][AssetRejectReason]. + static const values = [ + duplicate, + unsupportedFormat, + ]; + + static AssetRejectReason? fromJson(dynamic value) => AssetRejectReasonTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetRejectReason.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetRejectReason] to String, +/// and [decode] dynamic data back to [AssetRejectReason]. +class AssetRejectReasonTypeTransformer { + factory AssetRejectReasonTypeTransformer() => _instance ??= const AssetRejectReasonTypeTransformer._(); + + const AssetRejectReasonTypeTransformer._(); + + String encode(AssetRejectReason data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetRejectReason. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetRejectReason? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'duplicate': return AssetRejectReason.duplicate; + case r'unsupported-format': return AssetRejectReason.unsupportedFormat; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetRejectReasonTypeTransformer] instance. + static AssetRejectReasonTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 078dd0bdaf..324d12fcbf 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -15,8 +15,6 @@ class AssetResponseDto { AssetResponseDto({ required this.checksum, required this.createdAt, - required this.deviceAssetId, - required this.deviceId, this.duplicateId, required this.duration, this.exifInfo, @@ -56,17 +54,11 @@ class AssetResponseDto { /// The UTC timestamp when the asset was originally uploaded to Immich. DateTime createdAt; - /// Device asset ID - String deviceAssetId; - - /// Device ID - String deviceId; - /// Duplicate group ID String? duplicateId; - /// Video duration (for videos) - String duration; + /// Video/gif duration in hh:mm:ss.SSS format (null for static images) + String? duration; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -86,6 +78,8 @@ class AssetResponseDto { bool hasMetadata; /// Asset height + /// + /// Minimum value: 0 num? height; /// Asset ID @@ -159,7 +153,6 @@ class AssetResponseDto { /// Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting. String? thumbhash; - /// Asset type AssetTypeEnum type; List unassignedFaces; @@ -167,18 +160,17 @@ class AssetResponseDto { /// The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. DateTime updatedAt; - /// Asset visibility AssetVisibility visibility; /// Asset width + /// + /// Minimum value: 0 num? width; @override bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto && other.checksum == checksum && other.createdAt == createdAt && - other.deviceAssetId == deviceAssetId && - other.deviceId == deviceId && other.duplicateId == duplicateId && other.duration == duration && other.exifInfo == exifInfo && @@ -216,10 +208,8 @@ class AssetResponseDto { // ignore: unnecessary_parenthesis (checksum.hashCode) + (createdAt.hashCode) + - (deviceAssetId.hashCode) + - (deviceId.hashCode) + (duplicateId == null ? 0 : duplicateId!.hashCode) + - (duration.hashCode) + + (duration == null ? 0 : duration!.hashCode) + (exifInfo == null ? 0 : exifInfo!.hashCode) + (fileCreatedAt.hashCode) + (fileModifiedAt.hashCode) + @@ -251,20 +241,22 @@ class AssetResponseDto { (width == null ? 0 : width!.hashCode); @override - String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility, width=$width]'; + String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility, width=$width]'; Map toJson() { final json = {}; json[r'checksum'] = this.checksum; json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); - json[r'deviceAssetId'] = this.deviceAssetId; - json[r'deviceId'] = this.deviceId; if (this.duplicateId != null) { json[r'duplicateId'] = this.duplicateId; } else { // json[r'duplicateId'] = null; } + if (this.duration != null) { json[r'duration'] = this.duration; + } else { + // json[r'duration'] = null; + } if (this.exifInfo != null) { json[r'exifInfo'] = this.exifInfo; } else { @@ -348,10 +340,8 @@ class AssetResponseDto { return AssetResponseDto( checksum: mapValueOfType(json, r'checksum')!, createdAt: mapDateTime(json, r'createdAt', r'')!, - deviceAssetId: mapValueOfType(json, r'deviceAssetId')!, - deviceId: mapValueOfType(json, r'deviceId')!, duplicateId: mapValueOfType(json, r'duplicateId'), - duration: mapValueOfType(json, r'duration')!, + duration: mapValueOfType(json, r'duration'), exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']), fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!, fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!, @@ -434,8 +424,6 @@ class AssetResponseDto { static const requiredKeys = { 'checksum', 'createdAt', - 'deviceAssetId', - 'deviceId', 'duration', 'fileCreatedAt', 'fileModifiedAt', diff --git a/mobile/openapi/lib/model/asset_stack_response_dto.dart b/mobile/openapi/lib/model/asset_stack_response_dto.dart index 229e7aa710..96fd66a392 100644 --- a/mobile/openapi/lib/model/asset_stack_response_dto.dart +++ b/mobile/openapi/lib/model/asset_stack_response_dto.dart @@ -19,6 +19,9 @@ class AssetStackResponseDto { }); /// Number of assets in stack + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int assetCount; /// Stack ID diff --git a/mobile/openapi/lib/model/asset_stats_response_dto.dart b/mobile/openapi/lib/model/asset_stats_response_dto.dart index 201550c87f..df2762a2f3 100644 --- a/mobile/openapi/lib/model/asset_stats_response_dto.dart +++ b/mobile/openapi/lib/model/asset_stats_response_dto.dart @@ -19,12 +19,21 @@ class AssetStatsResponseDto { }); /// Number of images + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int images; /// Total number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; /// Number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/asset_upload_action.dart b/mobile/openapi/lib/model/asset_upload_action.dart new file mode 100644 index 0000000000..b5cdbb0151 --- /dev/null +++ b/mobile/openapi/lib/model/asset_upload_action.dart @@ -0,0 +1,85 @@ +// +// 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; + +/// Upload action +class AssetUploadAction { + /// Instantiate a new enum with the provided [value]. + const AssetUploadAction._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const accept = AssetUploadAction._(r'accept'); + static const reject = AssetUploadAction._(r'reject'); + + /// List of all possible values in this [enum][AssetUploadAction]. + static const values = [ + accept, + reject, + ]; + + static AssetUploadAction? fromJson(dynamic value) => AssetUploadActionTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetUploadAction.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetUploadAction] to String, +/// and [decode] dynamic data back to [AssetUploadAction]. +class AssetUploadActionTypeTransformer { + factory AssetUploadActionTypeTransformer() => _instance ??= const AssetUploadActionTypeTransformer._(); + + const AssetUploadActionTypeTransformer._(); + + String encode(AssetUploadAction data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetUploadAction. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetUploadAction? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'accept': return AssetUploadAction.accept; + case r'reject': return AssetUploadAction.reject; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetUploadActionTypeTransformer] instance. + static AssetUploadActionTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/avatar_update.dart b/mobile/openapi/lib/model/avatar_update.dart index a817832dab..875eb138a8 100644 --- a/mobile/openapi/lib/model/avatar_update.dart +++ b/mobile/openapi/lib/model/avatar_update.dart @@ -16,7 +16,6 @@ class AvatarUpdate { this.color, }); - /// Avatar color /// /// 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 diff --git a/mobile/openapi/lib/model/bulk_id_error_reason.dart b/mobile/openapi/lib/model/bulk_id_error_reason.dart index ea56e9dbba..fd6c61d6fd 100644 --- a/mobile/openapi/lib/model/bulk_id_error_reason.dart +++ b/mobile/openapi/lib/model/bulk_id_error_reason.dart @@ -27,6 +27,7 @@ class BulkIdErrorReason { static const noPermission = BulkIdErrorReason._(r'no_permission'); static const notFound = BulkIdErrorReason._(r'not_found'); static const unknown = BulkIdErrorReason._(r'unknown'); + static const validation = BulkIdErrorReason._(r'validation'); /// List of all possible values in this [enum][BulkIdErrorReason]. static const values = [ @@ -34,6 +35,7 @@ class BulkIdErrorReason { noPermission, notFound, unknown, + validation, ]; static BulkIdErrorReason? fromJson(dynamic value) => BulkIdErrorReasonTypeTransformer().decode(value); @@ -76,6 +78,7 @@ class BulkIdErrorReasonTypeTransformer { case r'no_permission': return BulkIdErrorReason.noPermission; case r'not_found': return BulkIdErrorReason.notFound; case r'unknown': return BulkIdErrorReason.unknown; + case r'validation': return BulkIdErrorReason.validation; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/bulk_id_response_dto.dart b/mobile/openapi/lib/model/bulk_id_response_dto.dart index cd122785dd..bb3f1d8856 100644 --- a/mobile/openapi/lib/model/bulk_id_response_dto.dart +++ b/mobile/openapi/lib/model/bulk_id_response_dto.dart @@ -14,12 +14,26 @@ class BulkIdResponseDto { /// Returns a new [BulkIdResponseDto] instance. BulkIdResponseDto({ this.error, + this.errorMessage, required this.id, required this.success, }); - /// Error reason if failed - BulkIdResponseDtoErrorEnum? error; + /// + /// 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. + /// + BulkIdErrorReason? error; + + /// + /// 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? errorMessage; /// ID String id; @@ -30,6 +44,7 @@ class BulkIdResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is BulkIdResponseDto && other.error == error && + other.errorMessage == errorMessage && other.id == id && other.success == success; @@ -37,11 +52,12 @@ class BulkIdResponseDto { int get hashCode => // ignore: unnecessary_parenthesis (error == null ? 0 : error!.hashCode) + + (errorMessage == null ? 0 : errorMessage!.hashCode) + (id.hashCode) + (success.hashCode); @override - String toString() => 'BulkIdResponseDto[error=$error, id=$id, success=$success]'; + String toString() => 'BulkIdResponseDto[error=$error, errorMessage=$errorMessage, id=$id, success=$success]'; Map toJson() { final json = {}; @@ -49,6 +65,11 @@ class BulkIdResponseDto { json[r'error'] = this.error; } else { // json[r'error'] = null; + } + if (this.errorMessage != null) { + json[r'errorMessage'] = this.errorMessage; + } else { + // json[r'errorMessage'] = null; } json[r'id'] = this.id; json[r'success'] = this.success; @@ -64,7 +85,8 @@ class BulkIdResponseDto { final json = value.cast(); return BulkIdResponseDto( - error: BulkIdResponseDtoErrorEnum.fromJson(json[r'error']), + error: BulkIdErrorReason.fromJson(json[r'error']), + errorMessage: mapValueOfType(json, r'errorMessage'), id: mapValueOfType(json, r'id')!, success: mapValueOfType(json, r'success')!, ); @@ -119,83 +141,3 @@ class BulkIdResponseDto { }; } -/// Error reason if failed -class BulkIdResponseDtoErrorEnum { - /// Instantiate a new enum with the provided [value]. - const BulkIdResponseDtoErrorEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const duplicate = BulkIdResponseDtoErrorEnum._(r'duplicate'); - static const noPermission = BulkIdResponseDtoErrorEnum._(r'no_permission'); - static const notFound = BulkIdResponseDtoErrorEnum._(r'not_found'); - static const unknown = BulkIdResponseDtoErrorEnum._(r'unknown'); - - /// List of all possible values in this [enum][BulkIdResponseDtoErrorEnum]. - static const values = [ - duplicate, - noPermission, - notFound, - unknown, - ]; - - static BulkIdResponseDtoErrorEnum? fromJson(dynamic value) => BulkIdResponseDtoErrorEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = BulkIdResponseDtoErrorEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [BulkIdResponseDtoErrorEnum] to String, -/// and [decode] dynamic data back to [BulkIdResponseDtoErrorEnum]. -class BulkIdResponseDtoErrorEnumTypeTransformer { - factory BulkIdResponseDtoErrorEnumTypeTransformer() => _instance ??= const BulkIdResponseDtoErrorEnumTypeTransformer._(); - - const BulkIdResponseDtoErrorEnumTypeTransformer._(); - - String encode(BulkIdResponseDtoErrorEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a BulkIdResponseDtoErrorEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - BulkIdResponseDtoErrorEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'duplicate': return BulkIdResponseDtoErrorEnum.duplicate; - case r'no_permission': return BulkIdResponseDtoErrorEnum.noPermission; - case r'not_found': return BulkIdResponseDtoErrorEnum.notFound; - case r'unknown': return BulkIdResponseDtoErrorEnum.unknown; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [BulkIdResponseDtoErrorEnumTypeTransformer] instance. - static BulkIdResponseDtoErrorEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/cast_response.dart b/mobile/openapi/lib/model/cast_response.dart index 0b7f0738fe..796138b0bf 100644 --- a/mobile/openapi/lib/model/cast_response.dart +++ b/mobile/openapi/lib/model/cast_response.dart @@ -13,7 +13,7 @@ part of openapi.api; class CastResponse { /// Returns a new [CastResponse] instance. CastResponse({ - this.gCastEnabled = false, + required this.gCastEnabled, }); /// Whether Google Cast is enabled diff --git a/mobile/openapi/lib/model/check_existing_assets_dto.dart b/mobile/openapi/lib/model/check_existing_assets_dto.dart deleted file mode 100644 index 6e4a471092..0000000000 --- a/mobile/openapi/lib/model/check_existing_assets_dto.dart +++ /dev/null @@ -1,111 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class CheckExistingAssetsDto { - /// Returns a new [CheckExistingAssetsDto] instance. - CheckExistingAssetsDto({ - this.deviceAssetIds = const [], - required this.deviceId, - }); - - /// Device asset IDs to check - List deviceAssetIds; - - /// Device ID - String deviceId; - - @override - bool operator ==(Object other) => identical(this, other) || other is CheckExistingAssetsDto && - _deepEquality.equals(other.deviceAssetIds, deviceAssetIds) && - other.deviceId == deviceId; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (deviceAssetIds.hashCode) + - (deviceId.hashCode); - - @override - String toString() => 'CheckExistingAssetsDto[deviceAssetIds=$deviceAssetIds, deviceId=$deviceId]'; - - Map toJson() { - final json = {}; - json[r'deviceAssetIds'] = this.deviceAssetIds; - json[r'deviceId'] = this.deviceId; - return json; - } - - /// Returns a new [CheckExistingAssetsDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static CheckExistingAssetsDto? fromJson(dynamic value) { - upgradeDto(value, "CheckExistingAssetsDto"); - if (value is Map) { - final json = value.cast(); - - return CheckExistingAssetsDto( - deviceAssetIds: json[r'deviceAssetIds'] is Iterable - ? (json[r'deviceAssetIds'] as Iterable).cast().toList(growable: false) - : const [], - deviceId: mapValueOfType(json, r'deviceId')!, - ); - } - 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 = CheckExistingAssetsDto.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 = CheckExistingAssetsDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of CheckExistingAssetsDto-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] = CheckExistingAssetsDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'deviceAssetIds', - 'deviceId', - }; -} - diff --git a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart b/mobile/openapi/lib/model/check_existing_assets_response_dto.dart deleted file mode 100644 index 9fb13f100f..0000000000 --- a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart +++ /dev/null @@ -1,102 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class CheckExistingAssetsResponseDto { - /// Returns a new [CheckExistingAssetsResponseDto] instance. - CheckExistingAssetsResponseDto({ - this.existingIds = const [], - }); - - /// Existing asset IDs - List existingIds; - - @override - bool operator ==(Object other) => identical(this, other) || other is CheckExistingAssetsResponseDto && - _deepEquality.equals(other.existingIds, existingIds); - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (existingIds.hashCode); - - @override - String toString() => 'CheckExistingAssetsResponseDto[existingIds=$existingIds]'; - - Map toJson() { - final json = {}; - json[r'existingIds'] = this.existingIds; - return json; - } - - /// Returns a new [CheckExistingAssetsResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static CheckExistingAssetsResponseDto? fromJson(dynamic value) { - upgradeDto(value, "CheckExistingAssetsResponseDto"); - if (value is Map) { - final json = value.cast(); - - return CheckExistingAssetsResponseDto( - existingIds: json[r'existingIds'] is Iterable - ? (json[r'existingIds'] as Iterable).cast().toList(growable: false) - : const [], - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = CheckExistingAssetsResponseDto.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 = CheckExistingAssetsResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of CheckExistingAssetsResponseDto-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] = CheckExistingAssetsResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'existingIds', - }; -} - diff --git a/mobile/openapi/lib/model/contributor_count_response_dto.dart b/mobile/openapi/lib/model/contributor_count_response_dto.dart index 1bef8f29d8..af5b2cbf68 100644 --- a/mobile/openapi/lib/model/contributor_count_response_dto.dart +++ b/mobile/openapi/lib/model/contributor_count_response_dto.dart @@ -18,6 +18,9 @@ class ContributorCountResponseDto { }); /// Number of assets contributed + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int assetCount; /// User ID diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart index 69942fee5c..ba12c62d76 100644 --- a/mobile/openapi/lib/model/create_library_dto.dart +++ b/mobile/openapi/lib/model/create_library_dto.dart @@ -13,17 +13,17 @@ part of openapi.api; class CreateLibraryDto { /// Returns a new [CreateLibraryDto] instance. CreateLibraryDto({ - this.exclusionPatterns = const {}, - this.importPaths = const {}, + this.exclusionPatterns = const [], + this.importPaths = const [], this.name, required this.ownerId, }); /// Exclusion patterns (max 128) - Set exclusionPatterns; + List exclusionPatterns; /// Import paths (max 128) - Set importPaths; + List importPaths; /// Library name /// @@ -57,8 +57,8 @@ class CreateLibraryDto { Map toJson() { final json = {}; - json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false); - json[r'importPaths'] = this.importPaths.toList(growable: false); + json[r'exclusionPatterns'] = this.exclusionPatterns; + json[r'importPaths'] = this.importPaths; if (this.name != null) { json[r'name'] = this.name; } else { @@ -78,11 +78,11 @@ class CreateLibraryDto { return CreateLibraryDto( exclusionPatterns: json[r'exclusionPatterns'] is Iterable - ? (json[r'exclusionPatterns'] as Iterable).cast().toSet() - : const {}, + ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) + : const [], importPaths: json[r'importPaths'] is Iterable - ? (json[r'importPaths'] as Iterable).cast().toSet() - : const {}, + ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) + : const [], name: mapValueOfType(json, r'name'), ownerId: mapValueOfType(json, r'ownerId')!, ); diff --git a/mobile/openapi/lib/model/create_profile_image_response_dto.dart b/mobile/openapi/lib/model/create_profile_image_response_dto.dart index 20d7cbd5e7..c6ec0d94a0 100644 --- a/mobile/openapi/lib/model/create_profile_image_response_dto.dart +++ b/mobile/openapi/lib/model/create_profile_image_response_dto.dart @@ -45,7 +45,9 @@ class CreateProfileImageResponseDto { Map toJson() { final json = {}; - json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); + json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.profileChangedAt.millisecondsSinceEpoch + : this.profileChangedAt.toUtc().toIso8601String(); json[r'profileImagePath'] = this.profileImagePath; json[r'userId'] = this.userId; return json; @@ -60,7 +62,7 @@ class CreateProfileImageResponseDto { final json = value.cast(); return CreateProfileImageResponseDto( - profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, userId: mapValueOfType(json, r'userId')!, ); diff --git a/mobile/openapi/lib/model/database_backup_delete_dto.dart b/mobile/openapi/lib/model/database_backup_delete_dto.dart index 8bc33a81dc..c336270b84 100644 --- a/mobile/openapi/lib/model/database_backup_delete_dto.dart +++ b/mobile/openapi/lib/model/database_backup_delete_dto.dart @@ -16,6 +16,7 @@ class DatabaseBackupDeleteDto { this.backups = const [], }); + /// Backup filenames to delete List backups; @override diff --git a/mobile/openapi/lib/model/database_backup_dto.dart b/mobile/openapi/lib/model/database_backup_dto.dart index 4bf231587b..abfa637157 100644 --- a/mobile/openapi/lib/model/database_backup_dto.dart +++ b/mobile/openapi/lib/model/database_backup_dto.dart @@ -15,30 +15,39 @@ class DatabaseBackupDto { DatabaseBackupDto({ required this.filename, required this.filesize, + required this.timezone, }); + /// Backup filename String filename; + /// Backup file size num filesize; + /// Backup timezone + String timezone; + @override bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupDto && other.filename == filename && - other.filesize == filesize; + other.filesize == filesize && + other.timezone == timezone; @override int get hashCode => // ignore: unnecessary_parenthesis (filename.hashCode) + - (filesize.hashCode); + (filesize.hashCode) + + (timezone.hashCode); @override - String toString() => 'DatabaseBackupDto[filename=$filename, filesize=$filesize]'; + String toString() => 'DatabaseBackupDto[filename=$filename, filesize=$filesize, timezone=$timezone]'; Map toJson() { final json = {}; json[r'filename'] = this.filename; json[r'filesize'] = this.filesize; + json[r'timezone'] = this.timezone; return json; } @@ -53,6 +62,7 @@ class DatabaseBackupDto { return DatabaseBackupDto( filename: mapValueOfType(json, r'filename')!, filesize: num.parse('${json[r'filesize']}'), + timezone: mapValueOfType(json, r'timezone')!, ); } return null; @@ -102,6 +112,7 @@ class DatabaseBackupDto { static const requiredKeys = { 'filename', 'filesize', + 'timezone', }; } diff --git a/mobile/openapi/lib/model/database_backup_list_response_dto.dart b/mobile/openapi/lib/model/database_backup_list_response_dto.dart index 16985dd605..de7bf78d5a 100644 --- a/mobile/openapi/lib/model/database_backup_list_response_dto.dart +++ b/mobile/openapi/lib/model/database_backup_list_response_dto.dart @@ -16,6 +16,7 @@ class DatabaseBackupListResponseDto { this.backups = const [], }); + /// List of backups List backups; @override diff --git a/mobile/openapi/lib/model/download_archive_info.dart b/mobile/openapi/lib/model/download_archive_info.dart index 97a3346a67..dcb1258457 100644 --- a/mobile/openapi/lib/model/download_archive_info.dart +++ b/mobile/openapi/lib/model/download_archive_info.dart @@ -21,6 +21,9 @@ class DownloadArchiveInfo { List assetIds; /// Archive size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int size; @override diff --git a/mobile/openapi/lib/model/download_info_dto.dart b/mobile/openapi/lib/model/download_info_dto.dart index a1ba44920e..8a0cebd945 100644 --- a/mobile/openapi/lib/model/download_info_dto.dart +++ b/mobile/openapi/lib/model/download_info_dto.dart @@ -31,6 +31,7 @@ class DownloadInfoDto { /// Archive size limit in bytes /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 /// /// 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 diff --git a/mobile/openapi/lib/model/download_response.dart b/mobile/openapi/lib/model/download_response.dart index 32e9487475..bc1d7b4047 100644 --- a/mobile/openapi/lib/model/download_response.dart +++ b/mobile/openapi/lib/model/download_response.dart @@ -14,10 +14,13 @@ class DownloadResponse { /// Returns a new [DownloadResponse] instance. DownloadResponse({ required this.archiveSize, - this.includeEmbeddedVideos = false, + required this.includeEmbeddedVideos, }); /// Maximum archive size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int archiveSize; /// Whether to include embedded videos in downloads diff --git a/mobile/openapi/lib/model/download_response_dto.dart b/mobile/openapi/lib/model/download_response_dto.dart index 81912e1d30..bfe32307fa 100644 --- a/mobile/openapi/lib/model/download_response_dto.dart +++ b/mobile/openapi/lib/model/download_response_dto.dart @@ -21,6 +21,9 @@ class DownloadResponseDto { List archives; /// Total size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int totalSize; @override diff --git a/mobile/openapi/lib/model/download_update.dart b/mobile/openapi/lib/model/download_update.dart index 4acc1c8bd3..c5feb9df43 100644 --- a/mobile/openapi/lib/model/download_update.dart +++ b/mobile/openapi/lib/model/download_update.dart @@ -20,6 +20,7 @@ class DownloadUpdate { /// Maximum archive size in bytes /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 /// /// 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 diff --git a/mobile/openapi/lib/model/server_theme_dto.dart b/mobile/openapi/lib/model/duplicate_resolve_dto.dart similarity index 52% rename from mobile/openapi/lib/model/server_theme_dto.dart rename to mobile/openapi/lib/model/duplicate_resolve_dto.dart index 957cf84d55..3466d3a620 100644 --- a/mobile/openapi/lib/model/server_theme_dto.dart +++ b/mobile/openapi/lib/model/duplicate_resolve_dto.dart @@ -10,53 +10,53 @@ part of openapi.api; -class ServerThemeDto { - /// Returns a new [ServerThemeDto] instance. - ServerThemeDto({ - required this.customCss, +class DuplicateResolveDto { + /// Returns a new [DuplicateResolveDto] instance. + DuplicateResolveDto({ + this.groups = const [], }); - /// Custom CSS for theming - String customCss; + /// List of duplicate groups to resolve + List groups; @override - bool operator ==(Object other) => identical(this, other) || other is ServerThemeDto && - other.customCss == customCss; + bool operator ==(Object other) => identical(this, other) || other is DuplicateResolveDto && + _deepEquality.equals(other.groups, groups); @override int get hashCode => // ignore: unnecessary_parenthesis - (customCss.hashCode); + (groups.hashCode); @override - String toString() => 'ServerThemeDto[customCss=$customCss]'; + String toString() => 'DuplicateResolveDto[groups=$groups]'; Map toJson() { final json = {}; - json[r'customCss'] = this.customCss; + json[r'groups'] = this.groups; return json; } - /// Returns a new [ServerThemeDto] instance and imports its values from + /// Returns a new [DuplicateResolveDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static ServerThemeDto? fromJson(dynamic value) { - upgradeDto(value, "ServerThemeDto"); + static DuplicateResolveDto? fromJson(dynamic value) { + upgradeDto(value, "DuplicateResolveDto"); if (value is Map) { final json = value.cast(); - return ServerThemeDto( - customCss: mapValueOfType(json, r'customCss')!, + return DuplicateResolveDto( + groups: DuplicateResolveGroupDto.listFromJson(json[r'groups']), ); } 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 = ServerThemeDto.fromJson(row); + final value = DuplicateResolveDto.fromJson(row); if (value != null) { result.add(value); } @@ -65,12 +65,12 @@ class ServerThemeDto { 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 = ServerThemeDto.fromJson(entry.value); + final value = DuplicateResolveDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -79,14 +79,14 @@ class ServerThemeDto { return map; } - // maps a json object with a list of ServerThemeDto-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 DuplicateResolveDto-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] = ServerThemeDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = DuplicateResolveDto.listFromJson(entry.value, growable: growable,); } } return map; @@ -94,7 +94,7 @@ class ServerThemeDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'customCss', + 'groups', }; } diff --git a/mobile/openapi/lib/model/duplicate_resolve_group_dto.dart b/mobile/openapi/lib/model/duplicate_resolve_group_dto.dart new file mode 100644 index 0000000000..94ca53eb7d --- /dev/null +++ b/mobile/openapi/lib/model/duplicate_resolve_group_dto.dart @@ -0,0 +1,121 @@ +// +// 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 DuplicateResolveGroupDto { + /// Returns a new [DuplicateResolveGroupDto] instance. + DuplicateResolveGroupDto({ + required this.duplicateId, + this.keepAssetIds = const [], + this.trashAssetIds = const [], + }); + + String duplicateId; + + /// Asset IDs to keep + List keepAssetIds; + + /// Asset IDs to trash or delete + List trashAssetIds; + + @override + bool operator ==(Object other) => identical(this, other) || other is DuplicateResolveGroupDto && + other.duplicateId == duplicateId && + _deepEquality.equals(other.keepAssetIds, keepAssetIds) && + _deepEquality.equals(other.trashAssetIds, trashAssetIds); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (duplicateId.hashCode) + + (keepAssetIds.hashCode) + + (trashAssetIds.hashCode); + + @override + String toString() => 'DuplicateResolveGroupDto[duplicateId=$duplicateId, keepAssetIds=$keepAssetIds, trashAssetIds=$trashAssetIds]'; + + Map toJson() { + final json = {}; + json[r'duplicateId'] = this.duplicateId; + json[r'keepAssetIds'] = this.keepAssetIds; + json[r'trashAssetIds'] = this.trashAssetIds; + return json; + } + + /// Returns a new [DuplicateResolveGroupDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static DuplicateResolveGroupDto? fromJson(dynamic value) { + upgradeDto(value, "DuplicateResolveGroupDto"); + if (value is Map) { + final json = value.cast(); + + return DuplicateResolveGroupDto( + duplicateId: mapValueOfType(json, r'duplicateId')!, + keepAssetIds: json[r'keepAssetIds'] is Iterable + ? (json[r'keepAssetIds'] as Iterable).cast().toList(growable: false) + : const [], + trashAssetIds: json[r'trashAssetIds'] is Iterable + ? (json[r'trashAssetIds'] as Iterable).cast().toList(growable: false) + : const [], + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = DuplicateResolveGroupDto.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 = DuplicateResolveGroupDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of DuplicateResolveGroupDto-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] = DuplicateResolveGroupDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'duplicateId', + 'keepAssetIds', + 'trashAssetIds', + }; +} + diff --git a/mobile/openapi/lib/model/duplicate_response_dto.dart b/mobile/openapi/lib/model/duplicate_response_dto.dart index 6c85dc8013..f0ddbb4fdd 100644 --- a/mobile/openapi/lib/model/duplicate_response_dto.dart +++ b/mobile/openapi/lib/model/duplicate_response_dto.dart @@ -15,6 +15,7 @@ class DuplicateResponseDto { DuplicateResponseDto({ this.assets = const [], required this.duplicateId, + this.suggestedKeepAssetIds = const [], }); /// Duplicate assets @@ -23,24 +24,30 @@ class DuplicateResponseDto { /// Duplicate group ID String duplicateId; + /// Suggested asset IDs to keep based on file size and EXIF data + List suggestedKeepAssetIds; + @override bool operator ==(Object other) => identical(this, other) || other is DuplicateResponseDto && _deepEquality.equals(other.assets, assets) && - other.duplicateId == duplicateId; + other.duplicateId == duplicateId && + _deepEquality.equals(other.suggestedKeepAssetIds, suggestedKeepAssetIds); @override int get hashCode => // ignore: unnecessary_parenthesis (assets.hashCode) + - (duplicateId.hashCode); + (duplicateId.hashCode) + + (suggestedKeepAssetIds.hashCode); @override - String toString() => 'DuplicateResponseDto[assets=$assets, duplicateId=$duplicateId]'; + String toString() => 'DuplicateResponseDto[assets=$assets, duplicateId=$duplicateId, suggestedKeepAssetIds=$suggestedKeepAssetIds]'; Map toJson() { final json = {}; json[r'assets'] = this.assets; json[r'duplicateId'] = this.duplicateId; + json[r'suggestedKeepAssetIds'] = this.suggestedKeepAssetIds; return json; } @@ -55,6 +62,9 @@ class DuplicateResponseDto { return DuplicateResponseDto( assets: AssetResponseDto.listFromJson(json[r'assets']), duplicateId: mapValueOfType(json, r'duplicateId')!, + suggestedKeepAssetIds: json[r'suggestedKeepAssetIds'] is Iterable + ? (json[r'suggestedKeepAssetIds'] as Iterable).cast().toList(growable: false) + : const [], ); } return null; @@ -104,6 +114,7 @@ class DuplicateResponseDto { static const requiredKeys = { 'assets', 'duplicateId', + 'suggestedKeepAssetIds', }; } diff --git a/mobile/openapi/lib/model/exif_response_dto.dart b/mobile/openapi/lib/model/exif_response_dto.dart index 6bb58a8ab9..64a5a73bed 100644 --- a/mobile/openapi/lib/model/exif_response_dto.dart +++ b/mobile/openapi/lib/model/exif_response_dto.dart @@ -50,9 +50,13 @@ class ExifResponseDto { String? description; /// Image height in pixels + /// + /// Minimum value: 0 num? exifImageHeight; /// Image width in pixels + /// + /// Minimum value: 0 num? exifImageWidth; /// Exposure time @@ -62,6 +66,9 @@ class ExifResponseDto { num? fNumber; /// File size in bytes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? fileSizeInByte; /// Focal length in mm diff --git a/mobile/openapi/lib/model/facial_recognition_config.dart b/mobile/openapi/lib/model/facial_recognition_config.dart index 4b9d7a6e9e..66cb542ccf 100644 --- a/mobile/openapi/lib/model/facial_recognition_config.dart +++ b/mobile/openapi/lib/model/facial_recognition_config.dart @@ -32,6 +32,7 @@ class FacialRecognitionConfig { /// Minimum number of faces required for recognition /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int minFaces; /// Minimum confidence score for face detection diff --git a/mobile/openapi/lib/model/folders_response.dart b/mobile/openapi/lib/model/folders_response.dart index 906a95a83c..873404c786 100644 --- a/mobile/openapi/lib/model/folders_response.dart +++ b/mobile/openapi/lib/model/folders_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class FoldersResponse { /// Returns a new [FoldersResponse] instance. FoldersResponse({ - this.enabled = false, - this.sidebarWeb = false, + required this.enabled, + required this.sidebarWeb, }); /// Whether folders are enabled diff --git a/mobile/openapi/lib/model/job_create_dto.dart b/mobile/openapi/lib/model/job_create_dto.dart index 3a3412384e..fe6743cba0 100644 --- a/mobile/openapi/lib/model/job_create_dto.dart +++ b/mobile/openapi/lib/model/job_create_dto.dart @@ -16,7 +16,6 @@ class JobCreateDto { required this.name, }); - /// Job name ManualJobName name; @override diff --git a/mobile/openapi/lib/model/job_name.dart b/mobile/openapi/lib/model/job_name.dart index 96b9339b7d..08f70569f8 100644 --- a/mobile/openapi/lib/model/job_name.dart +++ b/mobile/openapi/lib/model/job_name.dart @@ -38,7 +38,6 @@ class JobName { static const assetFileMigration = JobName._(r'AssetFileMigration'); static const assetGenerateThumbnailsQueueAll = JobName._(r'AssetGenerateThumbnailsQueueAll'); static const assetGenerateThumbnails = JobName._(r'AssetGenerateThumbnails'); - static const auditLogCleanup = JobName._(r'AuditLogCleanup'); static const auditTableCleanup = JobName._(r'AuditTableCleanup'); static const databaseBackup = JobName._(r'DatabaseBackup'); static const facialRecognitionQueueAll = JobName._(r'FacialRecognitionQueueAll'); @@ -97,7 +96,6 @@ class JobName { assetFileMigration, assetGenerateThumbnailsQueueAll, assetGenerateThumbnails, - auditLogCleanup, auditTableCleanup, databaseBackup, facialRecognitionQueueAll, @@ -191,7 +189,6 @@ class JobNameTypeTransformer { case r'AssetFileMigration': return JobName.assetFileMigration; case r'AssetGenerateThumbnailsQueueAll': return JobName.assetGenerateThumbnailsQueueAll; case r'AssetGenerateThumbnails': return JobName.assetGenerateThumbnails; - case r'AuditLogCleanup': return JobName.auditLogCleanup; case r'AuditTableCleanup': return JobName.auditTableCleanup; case r'DatabaseBackup': return JobName.databaseBackup; case r'FacialRecognitionQueueAll': return JobName.facialRecognitionQueueAll; diff --git a/mobile/openapi/lib/model/job_settings_dto.dart b/mobile/openapi/lib/model/job_settings_dto.dart index 73a0187ddd..98fe3d3536 100644 --- a/mobile/openapi/lib/model/job_settings_dto.dart +++ b/mobile/openapi/lib/model/job_settings_dto.dart @@ -19,6 +19,7 @@ class JobSettingsDto { /// Concurrency /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int concurrency; @override diff --git a/mobile/openapi/lib/model/library_response_dto.dart b/mobile/openapi/lib/model/library_response_dto.dart index aa9158e591..88ebceae24 100644 --- a/mobile/openapi/lib/model/library_response_dto.dart +++ b/mobile/openapi/lib/model/library_response_dto.dart @@ -25,6 +25,9 @@ class LibraryResponseDto { }); /// Number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int assetCount; /// Creation date @@ -82,18 +85,24 @@ class LibraryResponseDto { Map toJson() { final json = {}; json[r'assetCount'] = this.assetCount; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'exclusionPatterns'] = this.exclusionPatterns; json[r'id'] = this.id; json[r'importPaths'] = this.importPaths; json[r'name'] = this.name; json[r'ownerId'] = this.ownerId; if (this.refreshedAt != null) { - json[r'refreshedAt'] = this.refreshedAt!.toUtc().toIso8601String(); + json[r'refreshedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.refreshedAt!.millisecondsSinceEpoch + : this.refreshedAt!.toUtc().toIso8601String(); } else { // json[r'refreshedAt'] = null; } - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -107,7 +116,7 @@ class LibraryResponseDto { return LibraryResponseDto( assetCount: mapValueOfType(json, r'assetCount')!, - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, exclusionPatterns: json[r'exclusionPatterns'] is Iterable ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) : const [], @@ -117,8 +126,8 @@ class LibraryResponseDto { : const [], name: mapValueOfType(json, r'name')!, ownerId: mapValueOfType(json, r'ownerId')!, - refreshedAt: mapDateTime(json, r'refreshedAt', r''), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + refreshedAt: mapDateTime(json, r'refreshedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/library_stats_response_dto.dart b/mobile/openapi/lib/model/library_stats_response_dto.dart index 6eec3ae8d7..55adbc2b49 100644 --- a/mobile/openapi/lib/model/library_stats_response_dto.dart +++ b/mobile/openapi/lib/model/library_stats_response_dto.dart @@ -13,22 +13,34 @@ part of openapi.api; class LibraryStatsResponseDto { /// Returns a new [LibraryStatsResponseDto] instance. LibraryStatsResponseDto({ - this.photos = 0, - this.total = 0, - this.usage = 0, - this.videos = 0, + required this.photos, + required this.total, + required this.usage, + required this.videos, }); /// Number of photos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int photos; /// Total number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; /// Storage usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usage; /// Number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/license_key_dto.dart b/mobile/openapi/lib/model/license_key_dto.dart index ea1fee9d7a..d1818a2a43 100644 --- a/mobile/openapi/lib/model/license_key_dto.dart +++ b/mobile/openapi/lib/model/license_key_dto.dart @@ -20,7 +20,7 @@ class LicenseKeyDto { /// Activation key String activationKey; - /// License key (format: IM(SV|CL)(-XXXX){8}) + /// License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/) String licenseKey; @override diff --git a/mobile/openapi/lib/model/license_response_dto.dart b/mobile/openapi/lib/model/license_response_dto.dart deleted file mode 100644 index 84ff72c1eb..0000000000 --- a/mobile/openapi/lib/model/license_response_dto.dart +++ /dev/null @@ -1,118 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class LicenseResponseDto { - /// Returns a new [LicenseResponseDto] instance. - LicenseResponseDto({ - required this.activatedAt, - required this.activationKey, - required this.licenseKey, - }); - - /// Activation date - DateTime activatedAt; - - /// Activation key - String activationKey; - - /// License key (format: IM(SV|CL)(-XXXX){8}) - String licenseKey; - - @override - bool operator ==(Object other) => identical(this, other) || other is LicenseResponseDto && - other.activatedAt == activatedAt && - other.activationKey == activationKey && - other.licenseKey == licenseKey; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (activatedAt.hashCode) + - (activationKey.hashCode) + - (licenseKey.hashCode); - - @override - String toString() => 'LicenseResponseDto[activatedAt=$activatedAt, activationKey=$activationKey, licenseKey=$licenseKey]'; - - Map toJson() { - final json = {}; - json[r'activatedAt'] = this.activatedAt.toUtc().toIso8601String(); - json[r'activationKey'] = this.activationKey; - json[r'licenseKey'] = this.licenseKey; - return json; - } - - /// Returns a new [LicenseResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static LicenseResponseDto? fromJson(dynamic value) { - upgradeDto(value, "LicenseResponseDto"); - if (value is Map) { - final json = value.cast(); - - return LicenseResponseDto( - activatedAt: mapDateTime(json, r'activatedAt', r'')!, - activationKey: mapValueOfType(json, r'activationKey')!, - licenseKey: mapValueOfType(json, r'licenseKey')!, - ); - } - 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 = LicenseResponseDto.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 = LicenseResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of LicenseResponseDto-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] = LicenseResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'activatedAt', - 'activationKey', - 'licenseKey', - }; -} - diff --git a/mobile/openapi/lib/model/log_level.dart b/mobile/openapi/lib/model/log_level.dart index 2129096da2..edb6a1ddda 100644 --- a/mobile/openapi/lib/model/log_level.dart +++ b/mobile/openapi/lib/model/log_level.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Log level class LogLevel { /// Instantiate a new enum with the provided [value]. const LogLevel._(this.value); diff --git a/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart b/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart index ad524914b4..e3f8c0acbe 100644 --- a/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart +++ b/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart @@ -22,7 +22,6 @@ class MaintenanceDetectInstallStorageFolderDto { /// Number of files in the folder num files; - /// Storage folder StorageFolder folder; /// Whether the folder is readable diff --git a/mobile/openapi/lib/model/maintenance_status_response_dto.dart b/mobile/openapi/lib/model/maintenance_status_response_dto.dart index 52dbb5b95b..124fa674fd 100644 --- a/mobile/openapi/lib/model/maintenance_status_response_dto.dart +++ b/mobile/openapi/lib/model/maintenance_status_response_dto.dart @@ -20,7 +20,6 @@ class MaintenanceStatusResponseDto { this.task, }); - /// Maintenance action MaintenanceAction action; bool active; diff --git a/mobile/openapi/lib/model/manual_job_name.dart b/mobile/openapi/lib/model/manual_job_name.dart index d09790a81a..27753eb9dc 100644 --- a/mobile/openapi/lib/model/manual_job_name.dart +++ b/mobile/openapi/lib/model/manual_job_name.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Job name +/// Manual job name class ManualJobName { /// Instantiate a new enum with the provided [value]. const ManualJobName._(this.value); diff --git a/mobile/openapi/lib/model/memories_response.dart b/mobile/openapi/lib/model/memories_response.dart index 63d4094cd0..250e214a60 100644 --- a/mobile/openapi/lib/model/memories_response.dart +++ b/mobile/openapi/lib/model/memories_response.dart @@ -13,11 +13,14 @@ part of openapi.api; class MemoriesResponse { /// Returns a new [MemoriesResponse] instance. MemoriesResponse({ - this.duration = 5, - this.enabled = true, + required this.duration, + required this.enabled, }); /// Memory duration in seconds + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int duration; /// Whether memories are enabled diff --git a/mobile/openapi/lib/model/memories_update.dart b/mobile/openapi/lib/model/memories_update.dart index d27cef022d..ede9910d74 100644 --- a/mobile/openapi/lib/model/memories_update.dart +++ b/mobile/openapi/lib/model/memories_update.dart @@ -20,6 +20,7 @@ class MemoriesUpdate { /// Memory duration in seconds /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 /// /// 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 diff --git a/mobile/openapi/lib/model/memory_create_dto.dart b/mobile/openapi/lib/model/memory_create_dto.dart index 5b8eeed8fb..b906f6dd1d 100644 --- a/mobile/openapi/lib/model/memory_create_dto.dart +++ b/mobile/openapi/lib/model/memory_create_dto.dart @@ -67,7 +67,6 @@ class MemoryCreateDto { /// DateTime? showAt; - /// Memory type MemoryType type; @override @@ -101,7 +100,9 @@ class MemoryCreateDto { json[r'assetIds'] = this.assetIds; json[r'data'] = this.data; if (this.hideAt != null) { - json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + json[r'hideAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.hideAt!.millisecondsSinceEpoch + : this.hideAt!.toUtc().toIso8601String(); } else { // json[r'hideAt'] = null; } @@ -110,14 +111,20 @@ class MemoryCreateDto { } else { // json[r'isSaved'] = null; } - json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'memoryAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.memoryAt.millisecondsSinceEpoch + : this.memoryAt.toUtc().toIso8601String(); if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } if (this.showAt != null) { - json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + json[r'showAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.showAt!.millisecondsSinceEpoch + : this.showAt!.toUtc().toIso8601String(); } else { // json[r'showAt'] = null; } @@ -138,11 +145,11 @@ class MemoryCreateDto { ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) : const [], data: OnThisDayDto.fromJson(json[r'data'])!, - hideAt: mapDateTime(json, r'hideAt', r''), + hideAt: mapDateTime(json, r'hideAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), isSaved: mapValueOfType(json, r'isSaved'), - memoryAt: mapDateTime(json, r'memoryAt', r'')!, - seenAt: mapDateTime(json, r'seenAt', r''), - showAt: mapDateTime(json, r'showAt', r''), + memoryAt: mapDateTime(json, r'memoryAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + seenAt: mapDateTime(json, r'seenAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + showAt: mapDateTime(json, r'showAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: MemoryType.fromJson(json[r'type'])!, ); } diff --git a/mobile/openapi/lib/model/memory_response_dto.dart b/mobile/openapi/lib/model/memory_response_dto.dart index 1835095cf7..e736667d57 100644 --- a/mobile/openapi/lib/model/memory_response_dto.dart +++ b/mobile/openapi/lib/model/memory_response_dto.dart @@ -83,7 +83,6 @@ class MemoryResponseDto { /// DateTime? showAt; - /// Memory type MemoryType type; /// Last update date @@ -128,34 +127,48 @@ class MemoryResponseDto { Map toJson() { final json = {}; json[r'assets'] = this.assets; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'data'] = this.data; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } if (this.hideAt != null) { - json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + json[r'hideAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.hideAt!.millisecondsSinceEpoch + : this.hideAt!.toUtc().toIso8601String(); } else { // json[r'hideAt'] = null; } json[r'id'] = this.id; json[r'isSaved'] = this.isSaved; - json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'memoryAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.memoryAt.millisecondsSinceEpoch + : this.memoryAt.toUtc().toIso8601String(); json[r'ownerId'] = this.ownerId; if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } if (this.showAt != null) { - json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + json[r'showAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.showAt!.millisecondsSinceEpoch + : this.showAt!.toUtc().toIso8601String(); } else { // json[r'showAt'] = null; } json[r'type'] = this.type; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -169,18 +182,18 @@ class MemoryResponseDto { return MemoryResponseDto( assets: AssetResponseDto.listFromJson(json[r'assets']), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, data: OnThisDayDto.fromJson(json[r'data'])!, - deletedAt: mapDateTime(json, r'deletedAt', r''), - hideAt: mapDateTime(json, r'hideAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + hideAt: mapDateTime(json, r'hideAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), id: mapValueOfType(json, r'id')!, isSaved: mapValueOfType(json, r'isSaved')!, - memoryAt: mapDateTime(json, r'memoryAt', r'')!, + memoryAt: mapDateTime(json, r'memoryAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ownerId: mapValueOfType(json, r'ownerId')!, - seenAt: mapDateTime(json, r'seenAt', r''), - showAt: mapDateTime(json, r'showAt', r''), + seenAt: mapDateTime(json, r'seenAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + showAt: mapDateTime(json, r'showAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: MemoryType.fromJson(json[r'type'])!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/memory_search_order.dart b/mobile/openapi/lib/model/memory_search_order.dart index bdf5b59894..67d0b69f46 100644 --- a/mobile/openapi/lib/model/memory_search_order.dart +++ b/mobile/openapi/lib/model/memory_search_order.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Sort order class MemorySearchOrder { /// Instantiate a new enum with the provided [value]. const MemorySearchOrder._(this.value); diff --git a/mobile/openapi/lib/model/memory_statistics_response_dto.dart b/mobile/openapi/lib/model/memory_statistics_response_dto.dart index bde78de481..ae542870d9 100644 --- a/mobile/openapi/lib/model/memory_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/memory_statistics_response_dto.dart @@ -17,6 +17,9 @@ class MemoryStatisticsResponseDto { }); /// Total number of memories + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/memory_type.dart b/mobile/openapi/lib/model/memory_type.dart index aee7bd1ba1..ecfc93edb0 100644 --- a/mobile/openapi/lib/model/memory_type.dart +++ b/mobile/openapi/lib/model/memory_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Memory type class MemoryType { /// Instantiate a new enum with the provided [value]. const MemoryType._(this.value); diff --git a/mobile/openapi/lib/model/memory_update_dto.dart b/mobile/openapi/lib/model/memory_update_dto.dart index 4905b161bf..d8d7e9643b 100644 --- a/mobile/openapi/lib/model/memory_update_dto.dart +++ b/mobile/openapi/lib/model/memory_update_dto.dart @@ -69,12 +69,16 @@ class MemoryUpdateDto { // json[r'isSaved'] = null; } if (this.memoryAt != null) { - json[r'memoryAt'] = this.memoryAt!.toUtc().toIso8601String(); + json[r'memoryAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.memoryAt!.millisecondsSinceEpoch + : this.memoryAt!.toUtc().toIso8601String(); } else { // json[r'memoryAt'] = null; } if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } @@ -91,8 +95,8 @@ class MemoryUpdateDto { return MemoryUpdateDto( isSaved: mapValueOfType(json, r'isSaved'), - memoryAt: mapDateTime(json, r'memoryAt', r''), - seenAt: mapDateTime(json, r'seenAt', r''), + memoryAt: mapDateTime(json, r'memoryAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + seenAt: mapDateTime(json, r'seenAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), ); } return null; diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index 81f8d41527..d49ea7a4e5 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -20,8 +20,6 @@ class MetadataSearchDto { this.createdAfter, this.createdBefore, this.description, - this.deviceAssetId, - this.deviceId, this.encodedVideoPath, this.id, this.isEncoded, @@ -34,7 +32,7 @@ class MetadataSearchDto { this.make, this.model, this.ocr, - this.order = AssetOrder.desc, + this.order, this.originalFileName, this.originalPath, this.page, @@ -104,24 +102,6 @@ class MetadataSearchDto { /// String? description; - /// Filter by device asset ID - /// - /// 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? deviceAssetId; - - /// Device ID to filter by - /// - /// 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? deviceId; - /// Filter by encoded video file path /// /// Please note: This property should have been non-nullable! Since the specification file @@ -192,12 +172,6 @@ class MetadataSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -212,8 +186,13 @@ class MetadataSearchDto { /// String? ocr; - /// Sort order - AssetOrder order; + /// + /// 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. + /// + AssetOrder? order; /// Filter by original file name /// @@ -325,7 +304,6 @@ class MetadataSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// 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 @@ -352,7 +330,6 @@ class MetadataSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// 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 @@ -379,7 +356,7 @@ class MetadataSearchDto { /// bool? withExif; - /// Include assets with people + /// Include people data in response /// /// 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 @@ -406,8 +383,6 @@ class MetadataSearchDto { other.createdAfter == createdAfter && other.createdBefore == createdBefore && other.description == description && - other.deviceAssetId == deviceAssetId && - other.deviceId == deviceId && other.encodedVideoPath == encodedVideoPath && other.id == id && other.isEncoded == isEncoded && @@ -454,8 +429,6 @@ class MetadataSearchDto { (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + (description == null ? 0 : description!.hashCode) + - (deviceAssetId == null ? 0 : deviceAssetId!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) + (id == null ? 0 : id!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + @@ -468,7 +441,7 @@ class MetadataSearchDto { (make == null ? 0 : make!.hashCode) + (model == null ? 0 : model!.hashCode) + (ocr == null ? 0 : ocr!.hashCode) + - (order.hashCode) + + (order == null ? 0 : order!.hashCode) + (originalFileName == null ? 0 : originalFileName!.hashCode) + (originalPath == null ? 0 : originalPath!.hashCode) + (page == null ? 0 : page!.hashCode) + @@ -493,7 +466,7 @@ class MetadataSearchDto { (withStacked == null ? 0 : withStacked!.hashCode); @override - String toString() => 'MetadataSearchDto[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; + String toString() => 'MetadataSearchDto[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; Map toJson() { final json = {}; @@ -514,12 +487,16 @@ class MetadataSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } @@ -528,16 +505,6 @@ class MetadataSearchDto { } else { // json[r'description'] = null; } - if (this.deviceAssetId != null) { - json[r'deviceAssetId'] = this.deviceAssetId; - } else { - // json[r'deviceAssetId'] = null; - } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.encodedVideoPath != null) { json[r'encodedVideoPath'] = this.encodedVideoPath; } else { @@ -598,7 +565,11 @@ class MetadataSearchDto { } else { // json[r'ocr'] = null; } + if (this.order != null) { json[r'order'] = this.order; + } else { + // json[r'order'] = null; + } if (this.originalFileName != null) { json[r'originalFileName'] = this.originalFileName; } else { @@ -641,12 +612,16 @@ class MetadataSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } @@ -656,12 +631,16 @@ class MetadataSearchDto { // json[r'thumbnailPath'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -671,12 +650,16 @@ class MetadataSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -723,11 +706,9 @@ class MetadataSearchDto { checksum: mapValueOfType(json, r'checksum'), city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), + createdAfter: mapDateTime(json, r'createdAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + createdBefore: mapDateTime(json, r'createdBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), description: mapValueOfType(json, r'description'), - deviceAssetId: mapValueOfType(json, r'deviceAssetId'), - deviceId: mapValueOfType(json, r'deviceId'), encodedVideoPath: mapValueOfType(json, r'encodedVideoPath'), id: mapValueOfType(json, r'id'), isEncoded: mapValueOfType(json, r'isEncoded'), @@ -740,7 +721,7 @@ class MetadataSearchDto { make: mapValueOfType(json, r'make'), model: mapValueOfType(json, r'model'), ocr: mapValueOfType(json, r'ocr'), - order: AssetOrder.fromJson(json[r'order']) ?? AssetOrder.desc, + order: AssetOrder.fromJson(json[r'order']), originalFileName: mapValueOfType(json, r'originalFileName'), originalPath: mapValueOfType(json, r'originalPath'), page: num.parse('${json[r'page']}'), @@ -756,14 +737,14 @@ class MetadataSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + takenBefore: mapDateTime(json, r'takenBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), thumbnailPath: mapValueOfType(json, r'thumbnailPath'), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + trashedAfter: mapDateTime(json, r'trashedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), withDeleted: mapValueOfType(json, r'withDeleted'), withExif: mapValueOfType(json, r'withExif'), diff --git a/mobile/openapi/lib/model/mirror_parameters.dart b/mobile/openapi/lib/model/mirror_parameters.dart index e8b8db685b..78c3da786c 100644 --- a/mobile/openapi/lib/model/mirror_parameters.dart +++ b/mobile/openapi/lib/model/mirror_parameters.dart @@ -16,7 +16,6 @@ class MirrorParameters { required this.axis, }); - /// Axis to mirror along MirrorAxis axis; @override diff --git a/mobile/openapi/lib/model/notification_create_dto.dart b/mobile/openapi/lib/model/notification_create_dto.dart index 1288da8670..f9771246f9 100644 --- a/mobile/openapi/lib/model/notification_create_dto.dart +++ b/mobile/openapi/lib/model/notification_create_dto.dart @@ -13,7 +13,7 @@ part of openapi.api; class NotificationCreateDto { /// Returns a new [NotificationCreateDto] instance. NotificationCreateDto({ - this.data, + this.data = const {}, this.description, this.level, this.readAt, @@ -23,18 +23,11 @@ class NotificationCreateDto { }); /// Additional notification data - /// - /// 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. - /// - Object? data; + Map data; /// Notification description String? description; - /// Notification level /// /// 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 @@ -49,7 +42,6 @@ class NotificationCreateDto { /// Notification title String title; - /// Notification type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -63,7 +55,7 @@ class NotificationCreateDto { @override bool operator ==(Object other) => identical(this, other) || other is NotificationCreateDto && - other.data == data && + _deepEquality.equals(other.data, data) && other.description == description && other.level == level && other.readAt == readAt && @@ -74,7 +66,7 @@ class NotificationCreateDto { @override int get hashCode => // ignore: unnecessary_parenthesis - (data == null ? 0 : data!.hashCode) + + (data.hashCode) + (description == null ? 0 : description!.hashCode) + (level == null ? 0 : level!.hashCode) + (readAt == null ? 0 : readAt!.hashCode) + @@ -87,11 +79,7 @@ class NotificationCreateDto { Map toJson() { final json = {}; - if (this.data != null) { json[r'data'] = this.data; - } else { - // json[r'data'] = null; - } if (this.description != null) { json[r'description'] = this.description; } else { @@ -103,7 +91,9 @@ class NotificationCreateDto { // json[r'level'] = null; } if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -126,10 +116,10 @@ class NotificationCreateDto { final json = value.cast(); return NotificationCreateDto( - data: mapValueOfType(json, r'data'), + data: mapCastOfType(json, r'data') ?? const {}, description: mapValueOfType(json, r'description'), level: NotificationLevel.fromJson(json[r'level']), - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), title: mapValueOfType(json, r'title')!, type: NotificationType.fromJson(json[r'type']), userId: mapValueOfType(json, r'userId')!, diff --git a/mobile/openapi/lib/model/notification_dto.dart b/mobile/openapi/lib/model/notification_dto.dart index 30d43de115..ad0e79cb27 100644 --- a/mobile/openapi/lib/model/notification_dto.dart +++ b/mobile/openapi/lib/model/notification_dto.dart @@ -14,7 +14,7 @@ class NotificationDto { /// Returns a new [NotificationDto] instance. NotificationDto({ required this.createdAt, - this.data, + this.data = const {}, this.description, required this.id, required this.level, @@ -27,13 +27,7 @@ class NotificationDto { DateTime createdAt; /// Additional notification data - /// - /// 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. - /// - Object? data; + Map data; /// Notification description /// @@ -47,7 +41,6 @@ class NotificationDto { /// Notification ID String id; - /// Notification level NotificationLevel level; /// Date when notification was read @@ -62,13 +55,12 @@ class NotificationDto { /// Notification title String title; - /// Notification type NotificationType type; @override bool operator ==(Object other) => identical(this, other) || other is NotificationDto && other.createdAt == createdAt && - other.data == data && + _deepEquality.equals(other.data, data) && other.description == description && other.id == id && other.level == level && @@ -80,7 +72,7 @@ class NotificationDto { int get hashCode => // ignore: unnecessary_parenthesis (createdAt.hashCode) + - (data == null ? 0 : data!.hashCode) + + (data.hashCode) + (description == null ? 0 : description!.hashCode) + (id.hashCode) + (level.hashCode) + @@ -93,12 +85,10 @@ class NotificationDto { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); - if (this.data != null) { + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'data'] = this.data; - } else { - // json[r'data'] = null; - } if (this.description != null) { json[r'description'] = this.description; } else { @@ -107,7 +97,9 @@ class NotificationDto { json[r'id'] = this.id; json[r'level'] = this.level; if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -125,12 +117,12 @@ class NotificationDto { final json = value.cast(); return NotificationDto( - createdAt: mapDateTime(json, r'createdAt', r'')!, - data: mapValueOfType(json, r'data'), + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + data: mapCastOfType(json, r'data') ?? const {}, description: mapValueOfType(json, r'description'), id: mapValueOfType(json, r'id')!, level: NotificationLevel.fromJson(json[r'level'])!, - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), title: mapValueOfType(json, r'title')!, type: NotificationType.fromJson(json[r'type'])!, ); diff --git a/mobile/openapi/lib/model/notification_level.dart b/mobile/openapi/lib/model/notification_level.dart index 554863ae4f..4ca4e2bcc8 100644 --- a/mobile/openapi/lib/model/notification_level.dart +++ b/mobile/openapi/lib/model/notification_level.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Notification level class NotificationLevel { /// Instantiate a new enum with the provided [value]. const NotificationLevel._(this.value); diff --git a/mobile/openapi/lib/model/notification_type.dart b/mobile/openapi/lib/model/notification_type.dart index b5885aa441..dbc9c12f84 100644 --- a/mobile/openapi/lib/model/notification_type.dart +++ b/mobile/openapi/lib/model/notification_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Notification type class NotificationType { /// Instantiate a new enum with the provided [value]. const NotificationType._(this.value); diff --git a/mobile/openapi/lib/model/notification_update_all_dto.dart b/mobile/openapi/lib/model/notification_update_all_dto.dart index a157058324..5ac61ededc 100644 --- a/mobile/openapi/lib/model/notification_update_all_dto.dart +++ b/mobile/openapi/lib/model/notification_update_all_dto.dart @@ -41,7 +41,9 @@ class NotificationUpdateAllDto { final json = {}; json[r'ids'] = this.ids; if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -60,7 +62,7 @@ class NotificationUpdateAllDto { ids: json[r'ids'] is Iterable ? (json[r'ids'] as Iterable).cast().toList(growable: false) : const [], - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), ); } return null; diff --git a/mobile/openapi/lib/model/notification_update_dto.dart b/mobile/openapi/lib/model/notification_update_dto.dart index eddf9c7e12..c5d949d7b2 100644 --- a/mobile/openapi/lib/model/notification_update_dto.dart +++ b/mobile/openapi/lib/model/notification_update_dto.dart @@ -34,7 +34,9 @@ class NotificationUpdateDto { Map toJson() { final json = {}; if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -50,7 +52,7 @@ class NotificationUpdateDto { final json = value.cast(); return NotificationUpdateDto( - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), ); } return null; diff --git a/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart b/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart index 77466d61d9..b63f027af7 100644 --- a/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart +++ b/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Token endpoint auth method +/// OAuth token endpoint auth method class OAuthTokenEndpointAuthMethod { /// Instantiate a new enum with the provided [value]. const OAuthTokenEndpointAuthMethod._(this.value); diff --git a/mobile/openapi/lib/model/ocr_config.dart b/mobile/openapi/lib/model/ocr_config.dart index d97cd5ffca..2ce5646731 100644 --- a/mobile/openapi/lib/model/ocr_config.dart +++ b/mobile/openapi/lib/model/ocr_config.dart @@ -26,6 +26,7 @@ class OcrConfig { /// Maximum resolution for OCR processing /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int maxResolution; /// Minimum confidence score for text detection diff --git a/mobile/openapi/lib/model/on_this_day_dto.dart b/mobile/openapi/lib/model/on_this_day_dto.dart index 93ec956f58..77ae96532f 100644 --- a/mobile/openapi/lib/model/on_this_day_dto.dart +++ b/mobile/openapi/lib/model/on_this_day_dto.dart @@ -18,8 +18,9 @@ class OnThisDayDto { /// Year for on this day memory /// - /// Minimum value: 1 - num year; + /// Minimum value: 1000 + /// Maximum value: 9999 + int year; @override bool operator ==(Object other) => identical(this, other) || other is OnThisDayDto && @@ -48,7 +49,7 @@ class OnThisDayDto { final json = value.cast(); return OnThisDayDto( - year: num.parse('${json[r'year']}'), + year: mapValueOfType(json, r'year')!, ); } return null; diff --git a/mobile/openapi/lib/model/partner_direction.dart b/mobile/openapi/lib/model/partner_direction.dart index c43c0df75d..c5e3b308ac 100644 --- a/mobile/openapi/lib/model/partner_direction.dart +++ b/mobile/openapi/lib/model/partner_direction.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Partner direction class PartnerDirection { /// Instantiate a new enum with the provided [value]. const PartnerDirection._(this.value); diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart index 5789938d18..f4612cc98a 100644 --- a/mobile/openapi/lib/model/partner_response_dto.dart +++ b/mobile/openapi/lib/model/partner_response_dto.dart @@ -22,7 +22,6 @@ class PartnerResponseDto { required this.profileImagePath, }); - /// Avatar color UserAvatarColor avatarColor; /// User email diff --git a/mobile/openapi/lib/model/people_response.dart b/mobile/openapi/lib/model/people_response.dart index c09560e08c..9d5d8ec18a 100644 --- a/mobile/openapi/lib/model/people_response.dart +++ b/mobile/openapi/lib/model/people_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class PeopleResponse { /// Returns a new [PeopleResponse] instance. PeopleResponse({ - this.enabled = true, - this.sidebarWeb = false, + required this.enabled, + required this.sidebarWeb, }); /// Whether people are enabled diff --git a/mobile/openapi/lib/model/people_response_dto.dart b/mobile/openapi/lib/model/people_response_dto.dart index f345657e73..87edc6b4a7 100644 --- a/mobile/openapi/lib/model/people_response_dto.dart +++ b/mobile/openapi/lib/model/people_response_dto.dart @@ -29,12 +29,17 @@ class PeopleResponseDto { bool? hasNextPage; /// Number of hidden people + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int hidden; - /// List of people List people; /// Total number of people + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index 9092ede786..0ac9461027 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -41,7 +41,6 @@ class Permission { static const assetPeriodView = Permission._(r'asset.view'); static const assetPeriodDownload = Permission._(r'asset.download'); static const assetPeriodUpload = Permission._(r'asset.upload'); - static const assetPeriodReplace = Permission._(r'asset.replace'); static const assetPeriodCopy = Permission._(r'asset.copy'); static const assetPeriodDerive = Permission._(r'asset.derive'); static const assetPeriodEditPeriodGet = Permission._(r'asset.edit.get'); @@ -200,7 +199,6 @@ class Permission { assetPeriodView, assetPeriodDownload, assetPeriodUpload, - assetPeriodReplace, assetPeriodCopy, assetPeriodDerive, assetPeriodEditPeriodGet, @@ -394,7 +392,6 @@ class PermissionTypeTransformer { case r'asset.view': return Permission.assetPeriodView; case r'asset.download': return Permission.assetPeriodDownload; case r'asset.upload': return Permission.assetPeriodUpload; - case r'asset.replace': return Permission.assetPeriodReplace; case r'asset.copy': return Permission.assetPeriodCopy; case r'asset.derive': return Permission.assetPeriodDerive; case r'asset.edit.get': return Permission.assetPeriodEditPeriodGet; diff --git a/mobile/openapi/lib/model/person_statistics_response_dto.dart b/mobile/openapi/lib/model/person_statistics_response_dto.dart index d2b45c8ccb..aeac16cc8a 100644 --- a/mobile/openapi/lib/model/person_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/person_statistics_response_dto.dart @@ -17,6 +17,9 @@ class PersonStatisticsResponseDto { }); /// Number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int assets; @override diff --git a/mobile/openapi/lib/model/person_with_faces_response_dto.dart b/mobile/openapi/lib/model/person_with_faces_response_dto.dart index f31c04b69f..f710dff8b9 100644 --- a/mobile/openapi/lib/model/person_with_faces_response_dto.dart +++ b/mobile/openapi/lib/model/person_with_faces_response_dto.dart @@ -36,7 +36,6 @@ class PersonWithFacesResponseDto { /// String? color; - /// Face detections List faces; /// Person ID diff --git a/mobile/openapi/lib/model/plugin_action_response_dto.dart b/mobile/openapi/lib/model/plugin_action_response_dto.dart index 34fa314ba9..cff2dc92f7 100644 --- a/mobile/openapi/lib/model/plugin_action_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_action_response_dto.dart @@ -35,7 +35,7 @@ class PluginActionResponseDto { String pluginId; /// Action schema - Object? schema; + PluginJsonSchema? schema; /// Supported contexts List supportedContexts; @@ -96,7 +96,7 @@ class PluginActionResponseDto { id: mapValueOfType(json, r'id')!, methodName: mapValueOfType(json, r'methodName')!, pluginId: mapValueOfType(json, r'pluginId')!, - schema: mapValueOfType(json, r'schema'), + schema: PluginJsonSchema.fromJson(json[r'schema']), supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']), title: mapValueOfType(json, r'title')!, ); diff --git a/mobile/openapi/lib/model/plugin_context_type.dart b/mobile/openapi/lib/model/plugin_context_type.dart index 6f4ac91fdb..beda0b0f1a 100644 --- a/mobile/openapi/lib/model/plugin_context_type.dart +++ b/mobile/openapi/lib/model/plugin_context_type.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Context type +/// Plugin context class PluginContextType { /// Instantiate a new enum with the provided [value]. const PluginContextType._(this.value); diff --git a/mobile/openapi/lib/model/plugin_filter_response_dto.dart b/mobile/openapi/lib/model/plugin_filter_response_dto.dart index ea6411a9c1..d1ab867ff9 100644 --- a/mobile/openapi/lib/model/plugin_filter_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_filter_response_dto.dart @@ -35,7 +35,7 @@ class PluginFilterResponseDto { String pluginId; /// Filter schema - Object? schema; + PluginJsonSchema? schema; /// Supported contexts List supportedContexts; @@ -96,7 +96,7 @@ class PluginFilterResponseDto { id: mapValueOfType(json, r'id')!, methodName: mapValueOfType(json, r'methodName')!, pluginId: mapValueOfType(json, r'pluginId')!, - schema: mapValueOfType(json, r'schema'), + schema: PluginJsonSchema.fromJson(json[r'schema']), supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']), title: mapValueOfType(json, r'title')!, ); diff --git a/mobile/openapi/lib/model/plugin_json_schema.dart b/mobile/openapi/lib/model/plugin_json_schema.dart new file mode 100644 index 0000000000..f7a2d584d9 --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema.dart @@ -0,0 +1,158 @@ +// +// 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 PluginJsonSchema { + /// Returns a new [PluginJsonSchema] instance. + PluginJsonSchema({ + this.additionalProperties, + this.description, + this.properties = const {}, + this.required_ = const [], + this.type, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? additionalProperties; + + /// + /// 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? description; + + Map properties; + + List required_; + + /// + /// 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. + /// + PluginJsonSchemaType? type; + + @override + bool operator ==(Object other) => identical(this, other) || other is PluginJsonSchema && + other.additionalProperties == additionalProperties && + other.description == description && + _deepEquality.equals(other.properties, properties) && + _deepEquality.equals(other.required_, required_) && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (additionalProperties == null ? 0 : additionalProperties!.hashCode) + + (description == null ? 0 : description!.hashCode) + + (properties.hashCode) + + (required_.hashCode) + + (type == null ? 0 : type!.hashCode); + + @override + String toString() => 'PluginJsonSchema[additionalProperties=$additionalProperties, description=$description, properties=$properties, required_=$required_, type=$type]'; + + Map toJson() { + final json = {}; + if (this.additionalProperties != null) { + json[r'additionalProperties'] = this.additionalProperties; + } else { + // json[r'additionalProperties'] = null; + } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } + json[r'properties'] = this.properties; + json[r'required'] = this.required_; + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + return json; + } + + /// Returns a new [PluginJsonSchema] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PluginJsonSchema? fromJson(dynamic value) { + upgradeDto(value, "PluginJsonSchema"); + if (value is Map) { + final json = value.cast(); + + return PluginJsonSchema( + additionalProperties: mapValueOfType(json, r'additionalProperties'), + description: mapValueOfType(json, r'description'), + properties: PluginJsonSchemaProperty.mapFromJson(json[r'properties']), + required_: json[r'required'] is Iterable + ? (json[r'required'] as Iterable).cast().toList(growable: false) + : const [], + type: PluginJsonSchemaType.fromJson(json[r'type']), + ); + } + 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 = PluginJsonSchema.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 = PluginJsonSchema.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PluginJsonSchema-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] = PluginJsonSchema.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/plugin_json_schema_property.dart b/mobile/openapi/lib/model/plugin_json_schema_property.dart new file mode 100644 index 0000000000..65951da0a3 --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema_property.dart @@ -0,0 +1,195 @@ +// +// 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 PluginJsonSchemaProperty { + /// Returns a new [PluginJsonSchemaProperty] instance. + PluginJsonSchemaProperty({ + this.additionalProperties, + this.default_, + this.description, + this.enum_ = const [], + this.items, + this.properties = const {}, + this.required_ = const [], + this.type, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaPropertyAdditionalProperties? additionalProperties; + + Object? default_; + + /// + /// 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? description; + + List enum_; + + /// + /// 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. + /// + PluginJsonSchemaProperty? items; + + Map properties; + + List required_; + + /// + /// 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. + /// + PluginJsonSchemaType? type; + + @override + bool operator ==(Object other) => identical(this, other) || other is PluginJsonSchemaProperty && + other.additionalProperties == additionalProperties && + other.default_ == default_ && + other.description == description && + _deepEquality.equals(other.enum_, enum_) && + other.items == items && + _deepEquality.equals(other.properties, properties) && + _deepEquality.equals(other.required_, required_) && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (additionalProperties == null ? 0 : additionalProperties!.hashCode) + + (default_ == null ? 0 : default_!.hashCode) + + (description == null ? 0 : description!.hashCode) + + (enum_.hashCode) + + (items == null ? 0 : items!.hashCode) + + (properties.hashCode) + + (required_.hashCode) + + (type == null ? 0 : type!.hashCode); + + @override + String toString() => 'PluginJsonSchemaProperty[additionalProperties=$additionalProperties, default_=$default_, description=$description, enum_=$enum_, items=$items, properties=$properties, required_=$required_, type=$type]'; + + Map toJson() { + final json = {}; + if (this.additionalProperties != null) { + json[r'additionalProperties'] = this.additionalProperties; + } else { + // json[r'additionalProperties'] = null; + } + if (this.default_ != null) { + json[r'default'] = this.default_; + } else { + // json[r'default'] = null; + } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } + json[r'enum'] = this.enum_; + if (this.items != null) { + json[r'items'] = this.items; + } else { + // json[r'items'] = null; + } + json[r'properties'] = this.properties; + json[r'required'] = this.required_; + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + return json; + } + + /// Returns a new [PluginJsonSchemaProperty] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PluginJsonSchemaProperty? fromJson(dynamic value) { + upgradeDto(value, "PluginJsonSchemaProperty"); + if (value is Map) { + final json = value.cast(); + + return PluginJsonSchemaProperty( + additionalProperties: PluginJsonSchemaPropertyAdditionalProperties.fromJson(json[r'additionalProperties']), + default_: mapValueOfType(json, r'default'), + description: mapValueOfType(json, r'description'), + enum_: json[r'enum'] is Iterable + ? (json[r'enum'] as Iterable).cast().toList(growable: false) + : const [], + items: PluginJsonSchemaProperty.fromJson(json[r'items']), + properties: PluginJsonSchemaProperty.mapFromJson(json[r'properties']), + required_: json[r'required'] is Iterable + ? (json[r'required'] as Iterable).cast().toList(growable: false) + : const [], + type: PluginJsonSchemaType.fromJson(json[r'type']), + ); + } + 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 = PluginJsonSchemaProperty.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 = PluginJsonSchemaProperty.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PluginJsonSchemaProperty-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] = PluginJsonSchemaProperty.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/plugin_json_schema_property_additional_properties.dart b/mobile/openapi/lib/model/plugin_json_schema_property_additional_properties.dart new file mode 100644 index 0000000000..169c6be772 --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema_property_additional_properties.dart @@ -0,0 +1,195 @@ +// +// 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 PluginJsonSchemaPropertyAdditionalProperties { + /// Returns a new [PluginJsonSchemaPropertyAdditionalProperties] instance. + PluginJsonSchemaPropertyAdditionalProperties({ + this.additionalProperties, + this.default_, + this.description, + this.enum_ = const [], + this.items, + this.properties = const {}, + this.required_ = const [], + this.type, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaPropertyAdditionalProperties? additionalProperties; + + Object? default_; + + /// + /// 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? description; + + List enum_; + + /// + /// 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. + /// + PluginJsonSchemaProperty? items; + + Map properties; + + List required_; + + /// + /// 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. + /// + PluginJsonSchemaType? type; + + @override + bool operator ==(Object other) => identical(this, other) || other is PluginJsonSchemaPropertyAdditionalProperties && + other.additionalProperties == additionalProperties && + other.default_ == default_ && + other.description == description && + _deepEquality.equals(other.enum_, enum_) && + other.items == items && + _deepEquality.equals(other.properties, properties) && + _deepEquality.equals(other.required_, required_) && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (additionalProperties == null ? 0 : additionalProperties!.hashCode) + + (default_ == null ? 0 : default_!.hashCode) + + (description == null ? 0 : description!.hashCode) + + (enum_.hashCode) + + (items == null ? 0 : items!.hashCode) + + (properties.hashCode) + + (required_.hashCode) + + (type == null ? 0 : type!.hashCode); + + @override + String toString() => 'PluginJsonSchemaPropertyAdditionalProperties[additionalProperties=$additionalProperties, default_=$default_, description=$description, enum_=$enum_, items=$items, properties=$properties, required_=$required_, type=$type]'; + + Map toJson() { + final json = {}; + if (this.additionalProperties != null) { + json[r'additionalProperties'] = this.additionalProperties; + } else { + // json[r'additionalProperties'] = null; + } + if (this.default_ != null) { + json[r'default'] = this.default_; + } else { + // json[r'default'] = null; + } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } + json[r'enum'] = this.enum_; + if (this.items != null) { + json[r'items'] = this.items; + } else { + // json[r'items'] = null; + } + json[r'properties'] = this.properties; + json[r'required'] = this.required_; + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + return json; + } + + /// Returns a new [PluginJsonSchemaPropertyAdditionalProperties] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PluginJsonSchemaPropertyAdditionalProperties? fromJson(dynamic value) { + upgradeDto(value, "PluginJsonSchemaPropertyAdditionalProperties"); + if (value is Map) { + final json = value.cast(); + + return PluginJsonSchemaPropertyAdditionalProperties( + additionalProperties: PluginJsonSchemaPropertyAdditionalProperties.fromJson(json[r'additionalProperties']), + default_: mapValueOfType(json, r'default'), + description: mapValueOfType(json, r'description'), + enum_: json[r'enum'] is Iterable + ? (json[r'enum'] as Iterable).cast().toList(growable: false) + : const [], + items: PluginJsonSchemaProperty.fromJson(json[r'items']), + properties: PluginJsonSchemaProperty.mapFromJson(json[r'properties']), + required_: json[r'required'] is Iterable + ? (json[r'required'] as Iterable).cast().toList(growable: false) + : const [], + type: PluginJsonSchemaType.fromJson(json[r'type']), + ); + } + 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 = PluginJsonSchemaPropertyAdditionalProperties.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 = PluginJsonSchemaPropertyAdditionalProperties.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PluginJsonSchemaPropertyAdditionalProperties-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] = PluginJsonSchemaPropertyAdditionalProperties.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/plugin_json_schema_type.dart b/mobile/openapi/lib/model/plugin_json_schema_type.dart new file mode 100644 index 0000000000..cabac9b71b --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema_type.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class PluginJsonSchemaType { + /// Instantiate a new enum with the provided [value]. + const PluginJsonSchemaType._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const string = PluginJsonSchemaType._(r'string'); + static const number = PluginJsonSchemaType._(r'number'); + static const integer = PluginJsonSchemaType._(r'integer'); + static const boolean = PluginJsonSchemaType._(r'boolean'); + static const object = PluginJsonSchemaType._(r'object'); + static const array = PluginJsonSchemaType._(r'array'); + static const null_ = PluginJsonSchemaType._(r'null'); + + /// List of all possible values in this [enum][PluginJsonSchemaType]. + static const values = [ + string, + number, + integer, + boolean, + object, + array, + null_, + ]; + + static PluginJsonSchemaType? fromJson(dynamic value) => PluginJsonSchemaTypeTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PluginJsonSchemaType.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [PluginJsonSchemaType] to String, +/// and [decode] dynamic data back to [PluginJsonSchemaType]. +class PluginJsonSchemaTypeTypeTransformer { + factory PluginJsonSchemaTypeTypeTransformer() => _instance ??= const PluginJsonSchemaTypeTypeTransformer._(); + + const PluginJsonSchemaTypeTypeTransformer._(); + + String encode(PluginJsonSchemaType data) => data.value; + + /// Decodes a [dynamic value][data] to a PluginJsonSchemaType. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + PluginJsonSchemaType? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'string': return PluginJsonSchemaType.string; + case r'number': return PluginJsonSchemaType.number; + case r'integer': return PluginJsonSchemaType.integer; + case r'boolean': return PluginJsonSchemaType.boolean; + case r'object': return PluginJsonSchemaType.object; + case r'array': return PluginJsonSchemaType.array; + case r'null': return PluginJsonSchemaType.null_; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [PluginJsonSchemaTypeTypeTransformer] instance. + static PluginJsonSchemaTypeTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/plugin_trigger_response_dto.dart b/mobile/openapi/lib/model/plugin_trigger_response_dto.dart index 16a9604bcd..a6ee1c6b69 100644 --- a/mobile/openapi/lib/model/plugin_trigger_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_trigger_response_dto.dart @@ -17,10 +17,8 @@ class PluginTriggerResponseDto { required this.type, }); - /// Context type PluginContextType contextType; - /// Trigger type PluginTriggerType type; @override diff --git a/mobile/openapi/lib/model/plugin_trigger_type.dart b/mobile/openapi/lib/model/plugin_trigger_type.dart index 9ae64acf6c..3ebcef7a95 100644 --- a/mobile/openapi/lib/model/plugin_trigger_type.dart +++ b/mobile/openapi/lib/model/plugin_trigger_type.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Trigger type +/// Plugin trigger type class PluginTriggerType { /// Instantiate a new enum with the provided [value]. const PluginTriggerType._(this.value); diff --git a/mobile/openapi/lib/model/queue_command_dto.dart b/mobile/openapi/lib/model/queue_command_dto.dart index 9e1eea15db..fb68d85583 100644 --- a/mobile/openapi/lib/model/queue_command_dto.dart +++ b/mobile/openapi/lib/model/queue_command_dto.dart @@ -17,7 +17,6 @@ class QueueCommandDto { this.force, }); - /// Queue command to execute QueueCommand command; /// Force the command execution (if applicable) diff --git a/mobile/openapi/lib/model/queue_job_response_dto.dart b/mobile/openapi/lib/model/queue_job_response_dto.dart index 2ce63784eb..06d433edad 100644 --- a/mobile/openapi/lib/model/queue_job_response_dto.dart +++ b/mobile/openapi/lib/model/queue_job_response_dto.dart @@ -13,14 +13,14 @@ part of openapi.api; class QueueJobResponseDto { /// Returns a new [QueueJobResponseDto] instance. QueueJobResponseDto({ - required this.data, + this.data = const {}, this.id, required this.name, required this.timestamp, }); /// Job data payload - Object data; + Map data; /// Job ID /// @@ -31,15 +31,17 @@ class QueueJobResponseDto { /// String? id; - /// Job name JobName name; /// Job creation timestamp + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int timestamp; @override bool operator ==(Object other) => identical(this, other) || other is QueueJobResponseDto && - other.data == data && + _deepEquality.equals(other.data, data) && other.id == id && other.name == name && other.timestamp == timestamp; @@ -77,7 +79,7 @@ class QueueJobResponseDto { final json = value.cast(); return QueueJobResponseDto( - data: mapValueOfType(json, r'data')!, + data: mapCastOfType(json, r'data')!, id: mapValueOfType(json, r'id'), name: JobName.fromJson(json[r'name'])!, timestamp: mapValueOfType(json, r'timestamp')!, diff --git a/mobile/openapi/lib/model/queue_job_status.dart b/mobile/openapi/lib/model/queue_job_status.dart index 03a1371cc5..cbd01b11ed 100644 --- a/mobile/openapi/lib/model/queue_job_status.dart +++ b/mobile/openapi/lib/model/queue_job_status.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Queue job status class QueueJobStatus { /// Instantiate a new enum with the provided [value]. const QueueJobStatus._(this.value); diff --git a/mobile/openapi/lib/model/queue_name.dart b/mobile/openapi/lib/model/queue_name.dart index d94304d0d3..eb19d8957f 100644 --- a/mobile/openapi/lib/model/queue_name.dart +++ b/mobile/openapi/lib/model/queue_name.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Queue name class QueueName { /// Instantiate a new enum with the provided [value]. const QueueName._(this.value); diff --git a/mobile/openapi/lib/model/queue_response_dto.dart b/mobile/openapi/lib/model/queue_response_dto.dart index ac9244514c..c88f9fc195 100644 --- a/mobile/openapi/lib/model/queue_response_dto.dart +++ b/mobile/openapi/lib/model/queue_response_dto.dart @@ -21,7 +21,6 @@ class QueueResponseDto { /// Whether the queue is paused bool isPaused; - /// Queue name QueueName name; QueueStatisticsDto statistics; diff --git a/mobile/openapi/lib/model/queue_statistics_dto.dart b/mobile/openapi/lib/model/queue_statistics_dto.dart index c9a37ee30a..86c75f8e7c 100644 --- a/mobile/openapi/lib/model/queue_statistics_dto.dart +++ b/mobile/openapi/lib/model/queue_statistics_dto.dart @@ -22,21 +22,39 @@ class QueueStatisticsDto { }); /// Number of active jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int active; /// Number of completed jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int completed; /// Number of delayed jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int delayed; /// Number of failed jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int failed; /// Number of paused jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int paused; /// Number of waiting jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int waiting; @override diff --git a/mobile/openapi/lib/model/random_search_dto.dart b/mobile/openapi/lib/model/random_search_dto.dart index 4166fc9f3c..3f33d8f850 100644 --- a/mobile/openapi/lib/model/random_search_dto.dart +++ b/mobile/openapi/lib/model/random_search_dto.dart @@ -18,7 +18,6 @@ class RandomSearchDto { this.country, this.createdAfter, this.createdBefore, - this.deviceId, this.isEncoded, this.isFavorite, this.isMotion, @@ -75,15 +74,6 @@ class RandomSearchDto { /// DateTime? createdBefore; - /// Device ID to filter by - /// - /// 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? deviceId; - /// Filter by encoded status /// /// Please note: This property should have been non-nullable! Since the specification file @@ -136,12 +126,6 @@ class RandomSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -219,7 +203,6 @@ class RandomSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// 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 @@ -246,7 +229,6 @@ class RandomSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// 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 @@ -273,7 +255,7 @@ class RandomSearchDto { /// bool? withExif; - /// Include assets with people + /// Include people data in response /// /// 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 @@ -298,7 +280,6 @@ class RandomSearchDto { other.country == country && other.createdAfter == createdAfter && other.createdBefore == createdBefore && - other.deviceId == deviceId && other.isEncoded == isEncoded && other.isFavorite == isFavorite && other.isMotion == isMotion && @@ -335,7 +316,6 @@ class RandomSearchDto { (country == null ? 0 : country!.hashCode) + (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (isMotion == null ? 0 : isMotion!.hashCode) + @@ -365,7 +345,7 @@ class RandomSearchDto { (withStacked == null ? 0 : withStacked!.hashCode); @override - String toString() => 'RandomSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, 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, withPeople=$withPeople, withStacked=$withStacked]'; + String toString() => 'RandomSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, 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, withPeople=$withPeople, withStacked=$withStacked]'; Map toJson() { final json = {}; @@ -381,20 +361,19 @@ class RandomSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.isEncoded != null) { json[r'isEncoded'] = this.isEncoded; } else { @@ -467,22 +446,30 @@ class RandomSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -492,12 +479,16 @@ class RandomSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -543,9 +534,8 @@ class RandomSearchDto { : const [], city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), - deviceId: mapValueOfType(json, r'deviceId'), + createdAfter: mapDateTime(json, r'createdAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + createdBefore: mapDateTime(json, r'createdBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), isEncoded: mapValueOfType(json, r'isEncoded'), isFavorite: mapValueOfType(json, r'isFavorite'), isMotion: mapValueOfType(json, r'isMotion'), @@ -567,13 +557,13 @@ class RandomSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + takenBefore: mapDateTime(json, r'takenBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedAfter: mapDateTime(json, r'trashedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), withDeleted: mapValueOfType(json, r'withDeleted'), withExif: mapValueOfType(json, r'withExif'), diff --git a/mobile/openapi/lib/model/ratings_response.dart b/mobile/openapi/lib/model/ratings_response.dart index 4346fa5c58..7b067412bf 100644 --- a/mobile/openapi/lib/model/ratings_response.dart +++ b/mobile/openapi/lib/model/ratings_response.dart @@ -13,7 +13,7 @@ part of openapi.api; class RatingsResponse { /// Returns a new [RatingsResponse] instance. RatingsResponse({ - this.enabled = false, + required this.enabled, }); /// Whether ratings are enabled diff --git a/mobile/openapi/lib/model/reaction_level.dart b/mobile/openapi/lib/model/reaction_level.dart index 29568b9d11..6060f4c2b7 100644 --- a/mobile/openapi/lib/model/reaction_level.dart +++ b/mobile/openapi/lib/model/reaction_level.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Reaction level class ReactionLevel { /// Instantiate a new enum with the provided [value]. const ReactionLevel._(this.value); diff --git a/mobile/openapi/lib/model/reaction_type.dart b/mobile/openapi/lib/model/reaction_type.dart index 4c788138fb..c4daccad71 100644 --- a/mobile/openapi/lib/model/reaction_type.dart +++ b/mobile/openapi/lib/model/reaction_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Reaction type class ReactionType { /// Instantiate a new enum with the provided [value]. const ReactionType._(this.value); diff --git a/mobile/openapi/lib/model/search_album_response_dto.dart b/mobile/openapi/lib/model/search_album_response_dto.dart index 8841251e4a..c21113ee6d 100644 --- a/mobile/openapi/lib/model/search_album_response_dto.dart +++ b/mobile/openapi/lib/model/search_album_response_dto.dart @@ -20,6 +20,9 @@ class SearchAlbumResponseDto { }); /// Number of albums in this page + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int count; List facets; @@ -27,6 +30,9 @@ class SearchAlbumResponseDto { List items; /// Total number of matching albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/search_asset_response_dto.dart b/mobile/openapi/lib/model/search_asset_response_dto.dart index acb81f28e2..f4ffade26b 100644 --- a/mobile/openapi/lib/model/search_asset_response_dto.dart +++ b/mobile/openapi/lib/model/search_asset_response_dto.dart @@ -21,6 +21,9 @@ class SearchAssetResponseDto { }); /// Number of assets in this page + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int count; List facets; @@ -31,6 +34,9 @@ class SearchAssetResponseDto { String? nextPage; /// Total number of matching assets + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/search_facet_count_response_dto.dart b/mobile/openapi/lib/model/search_facet_count_response_dto.dart index 8318fbfb3b..62adfaa74a 100644 --- a/mobile/openapi/lib/model/search_facet_count_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_count_response_dto.dart @@ -18,6 +18,9 @@ class SearchFacetCountResponseDto { }); /// Number of assets with this facet value + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int count; /// Facet value diff --git a/mobile/openapi/lib/model/search_facet_response_dto.dart b/mobile/openapi/lib/model/search_facet_response_dto.dart index 43b5ac5c81..51124ef1cf 100644 --- a/mobile/openapi/lib/model/search_facet_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_response_dto.dart @@ -17,7 +17,6 @@ class SearchFacetResponseDto { required this.fieldName, }); - /// Facet counts List counts; /// Facet field name diff --git a/mobile/openapi/lib/model/search_statistics_response_dto.dart b/mobile/openapi/lib/model/search_statistics_response_dto.dart index 5aebe4d6a9..c4d893af05 100644 --- a/mobile/openapi/lib/model/search_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/search_statistics_response_dto.dart @@ -17,6 +17,9 @@ class SearchStatisticsResponseDto { }); /// Total number of matching assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/search_suggestion_type.dart b/mobile/openapi/lib/model/search_suggestion_type.dart index b18fe687c4..6d44b881bd 100644 --- a/mobile/openapi/lib/model/search_suggestion_type.dart +++ b/mobile/openapi/lib/model/search_suggestion_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Suggestion type class SearchSuggestionType { /// Instantiate a new enum with the provided [value]. const SearchSuggestionType._(this.value); diff --git a/mobile/openapi/lib/model/server_config_dto.dart b/mobile/openapi/lib/model/server_config_dto.dart index fec096d51a..316edb609f 100644 --- a/mobile/openapi/lib/model/server_config_dto.dart +++ b/mobile/openapi/lib/model/server_config_dto.dart @@ -54,9 +54,15 @@ class ServerConfigDto { bool publicUsers; /// Number of days before trashed assets are permanently deleted + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int trashDays; /// Delay in days before deleted users are permanently removed + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int userDeleteDelay; @override diff --git a/mobile/openapi/lib/model/server_stats_response_dto.dart b/mobile/openapi/lib/model/server_stats_response_dto.dart index ef2fa458e2..605bd74f41 100644 --- a/mobile/openapi/lib/model/server_stats_response_dto.dart +++ b/mobile/openapi/lib/model/server_stats_response_dto.dart @@ -13,29 +13,45 @@ part of openapi.api; class ServerStatsResponseDto { /// Returns a new [ServerStatsResponseDto] instance. ServerStatsResponseDto({ - this.photos = 0, - this.usage = 0, + required this.photos, + required this.usage, this.usageByUser = const [], - this.usagePhotos = 0, - this.usageVideos = 0, - this.videos = 0, + required this.usagePhotos, + required this.usageVideos, + required this.videos, }); /// Total number of photos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int photos; /// Total storage usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usage; + /// Array of usage for each user List usageByUser; /// Storage usage for photos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usagePhotos; /// Storage usage for videos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usageVideos; /// Total number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/server_storage_response_dto.dart b/mobile/openapi/lib/model/server_storage_response_dto.dart index 476b048b4d..4a66d54e37 100644 --- a/mobile/openapi/lib/model/server_storage_response_dto.dart +++ b/mobile/openapi/lib/model/server_storage_response_dto.dart @@ -26,12 +26,18 @@ class ServerStorageResponseDto { String diskAvailable; /// Available disk space in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int diskAvailableRaw; /// Total disk size (human-readable format) String diskSize; /// Total disk size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int diskSizeRaw; /// Disk usage percentage (0-100) @@ -41,6 +47,9 @@ class ServerStorageResponseDto { String diskUse; /// Used disk space in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int diskUseRaw; @override diff --git a/mobile/openapi/lib/model/server_version_history_response_dto.dart b/mobile/openapi/lib/model/server_version_history_response_dto.dart index c3b7049016..ae5e060cff 100644 --- a/mobile/openapi/lib/model/server_version_history_response_dto.dart +++ b/mobile/openapi/lib/model/server_version_history_response_dto.dart @@ -45,7 +45,9 @@ class ServerVersionHistoryResponseDto { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'version'] = this.version; return json; @@ -60,7 +62,7 @@ class ServerVersionHistoryResponseDto { final json = value.cast(); return ServerVersionHistoryResponseDto( - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, id: mapValueOfType(json, r'id')!, version: mapValueOfType(json, r'version')!, ); diff --git a/mobile/openapi/lib/model/server_version_response_dto.dart b/mobile/openapi/lib/model/server_version_response_dto.dart index a13cd81ad7..60161a7458 100644 --- a/mobile/openapi/lib/model/server_version_response_dto.dart +++ b/mobile/openapi/lib/model/server_version_response_dto.dart @@ -19,12 +19,21 @@ class ServerVersionResponseDto { }); /// Major version number + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int major; /// Minor version number + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int minor; /// Patch version number + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int patch_; @override diff --git a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart index 14bf584bb9..e7c9dc0d63 100644 --- a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart +++ b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart @@ -17,7 +17,6 @@ class SetMaintenanceModeDto { this.restoreBackupFilename, }); - /// Maintenance action MaintenanceAction action; /// Restore backup filename diff --git a/mobile/openapi/lib/model/shared_link_create_dto.dart b/mobile/openapi/lib/model/shared_link_create_dto.dart index 2675ad4beb..a32714d556 100644 --- a/mobile/openapi/lib/model/shared_link_create_dto.dart +++ b/mobile/openapi/lib/model/shared_link_create_dto.dart @@ -64,7 +64,6 @@ class SharedLinkCreateDto { /// Custom URL slug String? slug; - /// Shared link type SharedLinkType type; @override @@ -117,7 +116,9 @@ class SharedLinkCreateDto { // json[r'description'] = null; } if (this.expiresAt != null) { - json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); + json[r'expiresAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.expiresAt!.millisecondsSinceEpoch + : this.expiresAt!.toUtc().toIso8601String(); } else { // json[r'expiresAt'] = null; } @@ -152,7 +153,7 @@ class SharedLinkCreateDto { ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) : const [], description: mapValueOfType(json, r'description'), - expiresAt: mapDateTime(json, r'expiresAt', r''), + expiresAt: mapDateTime(json, r'expiresAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), password: mapValueOfType(json, r'password'), showMetadata: mapValueOfType(json, r'showMetadata') ?? true, slug: mapValueOfType(json, r'slug'), diff --git a/mobile/openapi/lib/model/shared_link_edit_dto.dart b/mobile/openapi/lib/model/shared_link_edit_dto.dart index b22232add6..11d6cdd52e 100644 --- a/mobile/openapi/lib/model/shared_link_edit_dto.dart +++ b/mobile/openapi/lib/model/shared_link_edit_dto.dart @@ -120,7 +120,9 @@ class SharedLinkEditDto { // json[r'description'] = null; } if (this.expiresAt != null) { - json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); + json[r'expiresAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.expiresAt!.millisecondsSinceEpoch + : this.expiresAt!.toUtc().toIso8601String(); } else { // json[r'expiresAt'] = null; } @@ -155,7 +157,7 @@ class SharedLinkEditDto { allowUpload: mapValueOfType(json, r'allowUpload'), changeExpiryTime: mapValueOfType(json, r'changeExpiryTime'), description: mapValueOfType(json, r'description'), - expiresAt: mapDateTime(json, r'expiresAt', r''), + expiresAt: mapDateTime(json, r'expiresAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), password: mapValueOfType(json, r'password'), showMetadata: mapValueOfType(json, r'showMetadata'), slug: mapValueOfType(json, r'slug'), diff --git a/mobile/openapi/lib/model/shared_link_response_dto.dart b/mobile/openapi/lib/model/shared_link_response_dto.dart index d9aec48c39..bad0966ca2 100644 --- a/mobile/openapi/lib/model/shared_link_response_dto.dart +++ b/mobile/openapi/lib/model/shared_link_response_dto.dart @@ -25,7 +25,6 @@ class SharedLinkResponseDto { required this.password, required this.showMetadata, required this.slug, - this.token, required this.type, required this.userId, }); @@ -70,10 +69,6 @@ class SharedLinkResponseDto { /// Custom URL slug String? slug; - /// Access token - String? token; - - /// Shared link type SharedLinkType type; /// Owner user ID @@ -93,7 +88,6 @@ class SharedLinkResponseDto { other.password == password && other.showMetadata == showMetadata && other.slug == slug && - other.token == token && other.type == type && other.userId == userId; @@ -112,12 +106,11 @@ class SharedLinkResponseDto { (password == null ? 0 : password!.hashCode) + (showMetadata.hashCode) + (slug == null ? 0 : slug!.hashCode) + - (token == null ? 0 : token!.hashCode) + (type.hashCode) + (userId.hashCode); @override - String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, password=$password, showMetadata=$showMetadata, slug=$slug, token=$token, type=$type, userId=$userId]'; + String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, password=$password, showMetadata=$showMetadata, slug=$slug, type=$type, userId=$userId]'; Map toJson() { final json = {}; @@ -129,14 +122,18 @@ class SharedLinkResponseDto { json[r'allowDownload'] = this.allowDownload; json[r'allowUpload'] = this.allowUpload; json[r'assets'] = this.assets; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); if (this.description != null) { json[r'description'] = this.description; } else { // json[r'description'] = null; } if (this.expiresAt != null) { - json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); + json[r'expiresAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.expiresAt!.millisecondsSinceEpoch + : this.expiresAt!.toUtc().toIso8601String(); } else { // json[r'expiresAt'] = null; } @@ -152,11 +149,6 @@ class SharedLinkResponseDto { json[r'slug'] = this.slug; } else { // json[r'slug'] = null; - } - if (this.token != null) { - json[r'token'] = this.token; - } else { - // json[r'token'] = null; } json[r'type'] = this.type; json[r'userId'] = this.userId; @@ -176,15 +168,14 @@ class SharedLinkResponseDto { allowDownload: mapValueOfType(json, r'allowDownload')!, allowUpload: mapValueOfType(json, r'allowUpload')!, assets: AssetResponseDto.listFromJson(json[r'assets']), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, description: mapValueOfType(json, r'description'), - expiresAt: mapDateTime(json, r'expiresAt', r''), + expiresAt: mapDateTime(json, r'expiresAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), id: mapValueOfType(json, r'id')!, key: mapValueOfType(json, r'key')!, password: mapValueOfType(json, r'password'), showMetadata: mapValueOfType(json, r'showMetadata')!, slug: mapValueOfType(json, r'slug'), - token: mapValueOfType(json, r'token'), type: SharedLinkType.fromJson(json[r'type'])!, userId: mapValueOfType(json, r'userId')!, ); diff --git a/mobile/openapi/lib/model/shared_links_response.dart b/mobile/openapi/lib/model/shared_links_response.dart index 510e94e43f..2b32a57540 100644 --- a/mobile/openapi/lib/model/shared_links_response.dart +++ b/mobile/openapi/lib/model/shared_links_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class SharedLinksResponse { /// Returns a new [SharedLinksResponse] instance. SharedLinksResponse({ - this.enabled = true, - this.sidebarWeb = false, + required this.enabled, + required this.sidebarWeb, }); /// Whether shared links are enabled diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index 5f8214467f..bf1465223e 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -18,7 +18,6 @@ class SmartSearchDto { this.country, this.createdAfter, this.createdBefore, - this.deviceId, this.isEncoded, this.isFavorite, this.isMotion, @@ -77,15 +76,6 @@ class SmartSearchDto { /// DateTime? createdBefore; - /// Device ID to filter by - /// - /// 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? deviceId; - /// Filter by encoded status /// /// Please note: This property should have been non-nullable! Since the specification file @@ -147,12 +137,6 @@ class SmartSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -259,7 +243,6 @@ class SmartSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// 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 @@ -286,7 +269,6 @@ class SmartSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// 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 @@ -320,7 +302,6 @@ class SmartSearchDto { other.country == country && other.createdAfter == createdAfter && other.createdBefore == createdBefore && - other.deviceId == deviceId && other.isEncoded == isEncoded && other.isFavorite == isFavorite && other.isMotion == isMotion && @@ -359,7 +340,6 @@ class SmartSearchDto { (country == null ? 0 : country!.hashCode) + (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (isMotion == null ? 0 : isMotion!.hashCode) + @@ -391,7 +371,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, ocr=$ocr, 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]'; + String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, 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 = {}; @@ -407,20 +387,19 @@ class SmartSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.isEncoded != null) { json[r'isEncoded'] = this.isEncoded; } else { @@ -513,22 +492,30 @@ class SmartSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -538,12 +525,16 @@ class SmartSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -579,9 +570,8 @@ class SmartSearchDto { : const [], city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), - deviceId: mapValueOfType(json, r'deviceId'), + createdAfter: mapDateTime(json, r'createdAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + createdBefore: mapDateTime(json, r'createdBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), isEncoded: mapValueOfType(json, r'isEncoded'), isFavorite: mapValueOfType(json, r'isFavorite'), isMotion: mapValueOfType(json, r'isMotion'), @@ -607,13 +597,13 @@ class SmartSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + takenBefore: mapDateTime(json, r'takenBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedAfter: mapDateTime(json, r'trashedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), withDeleted: mapValueOfType(json, r'withDeleted'), withExif: mapValueOfType(json, r'withExif'), diff --git a/mobile/openapi/lib/model/stack_response_dto.dart b/mobile/openapi/lib/model/stack_response_dto.dart index 638dfb5255..326f83a03d 100644 --- a/mobile/openapi/lib/model/stack_response_dto.dart +++ b/mobile/openapi/lib/model/stack_response_dto.dart @@ -18,7 +18,6 @@ class StackResponseDto { required this.primaryAssetId, }); - /// Stack assets List assets; /// Stack ID diff --git a/mobile/openapi/lib/model/statistics_search_dto.dart b/mobile/openapi/lib/model/statistics_search_dto.dart index d5bbf448a3..d0070e8e12 100644 --- a/mobile/openapi/lib/model/statistics_search_dto.dart +++ b/mobile/openapi/lib/model/statistics_search_dto.dart @@ -19,7 +19,6 @@ class StatisticsSearchDto { this.createdAfter, this.createdBefore, this.description, - this.deviceId, this.isEncoded, this.isFavorite, this.isMotion, @@ -80,15 +79,6 @@ class StatisticsSearchDto { /// String? description; - /// Device ID to filter by - /// - /// 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? deviceId; - /// Filter by encoded status /// /// Please note: This property should have been non-nullable! Since the specification file @@ -141,12 +131,6 @@ class StatisticsSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -212,7 +196,6 @@ class StatisticsSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// 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 @@ -239,7 +222,6 @@ class StatisticsSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// 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 @@ -256,7 +238,6 @@ class StatisticsSearchDto { other.createdAfter == createdAfter && other.createdBefore == createdBefore && other.description == description && - other.deviceId == deviceId && other.isEncoded == isEncoded && other.isFavorite == isFavorite && other.isMotion == isMotion && @@ -289,7 +270,6 @@ class StatisticsSearchDto { (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + (description == null ? 0 : description!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (isMotion == null ? 0 : isMotion!.hashCode) + @@ -314,7 +294,7 @@ class StatisticsSearchDto { (visibility == null ? 0 : visibility!.hashCode); @override - String toString() => 'StatisticsSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]'; + String toString() => 'StatisticsSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]'; Map toJson() { final json = {}; @@ -330,12 +310,16 @@ class StatisticsSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } @@ -344,11 +328,6 @@ class StatisticsSearchDto { } else { // json[r'description'] = null; } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.isEncoded != null) { json[r'isEncoded'] = this.isEncoded; } else { @@ -416,22 +395,30 @@ class StatisticsSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -441,12 +428,16 @@ class StatisticsSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -472,10 +463,9 @@ class StatisticsSearchDto { : const [], city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), + createdAfter: mapDateTime(json, r'createdAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + createdBefore: mapDateTime(json, r'createdBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), description: mapValueOfType(json, r'description'), - deviceId: mapValueOfType(json, r'deviceId'), isEncoded: mapValueOfType(json, r'isEncoded'), isFavorite: mapValueOfType(json, r'isFavorite'), isMotion: mapValueOfType(json, r'isMotion'), @@ -496,13 +486,13 @@ class StatisticsSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + takenBefore: mapDateTime(json, r'takenBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedAfter: mapDateTime(json, r'trashedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), ); } diff --git a/mobile/openapi/lib/model/sync_ack_dto.dart b/mobile/openapi/lib/model/sync_ack_dto.dart index 747f671557..fa7e20a832 100644 --- a/mobile/openapi/lib/model/sync_ack_dto.dart +++ b/mobile/openapi/lib/model/sync_ack_dto.dart @@ -20,7 +20,6 @@ class SyncAckDto { /// Acknowledgment ID String ack; - /// Sync entity type SyncEntityType type; @override diff --git a/mobile/openapi/lib/model/sync_album_user_v1.dart b/mobile/openapi/lib/model/sync_album_user_v1.dart index 3fc8972069..1efe7da029 100644 --- a/mobile/openapi/lib/model/sync_album_user_v1.dart +++ b/mobile/openapi/lib/model/sync_album_user_v1.dart @@ -21,7 +21,6 @@ class SyncAlbumUserV1 { /// Album ID String albumId; - /// Album user role AlbumUserRole role; /// User ID diff --git a/mobile/openapi/lib/model/sync_album_v1.dart b/mobile/openapi/lib/model/sync_album_v1.dart index 6c89d93724..17b2bda02b 100644 --- a/mobile/openapi/lib/model/sync_album_v1.dart +++ b/mobile/openapi/lib/model/sync_album_v1.dart @@ -80,7 +80,9 @@ class SyncAlbumV1 { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'description'] = this.description; json[r'id'] = this.id; json[r'isActivityEnabled'] = this.isActivityEnabled; @@ -92,7 +94,9 @@ class SyncAlbumV1 { } else { // json[r'thumbnailAssetId'] = null; } - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -105,7 +109,7 @@ class SyncAlbumV1 { final json = value.cast(); return SyncAlbumV1( - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, description: mapValueOfType(json, r'description')!, id: mapValueOfType(json, r'id')!, isActivityEnabled: mapValueOfType(json, r'isActivityEnabled')!, @@ -113,7 +117,7 @@ class SyncAlbumV1 { order: AssetOrder.fromJson(json[r'order'])!, ownerId: mapValueOfType(json, r'ownerId')!, thumbnailAssetId: mapValueOfType(json, r'thumbnailAssetId'), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart b/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart index 68af280290..e0c98bfef3 100644 --- a/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart @@ -16,6 +16,7 @@ class SyncAssetEditDeleteV1 { required this.editId, }); + /// Edit ID String editId; @override diff --git a/mobile/openapi/lib/model/sync_asset_edit_v1.dart b/mobile/openapi/lib/model/sync_asset_edit_v1.dart index 3cc2673bfc..8acfad5f6a 100644 --- a/mobile/openapi/lib/model/sync_asset_edit_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_edit_v1.dart @@ -16,18 +16,25 @@ class SyncAssetEditV1 { required this.action, required this.assetId, required this.id, - required this.parameters, + this.parameters = const {}, required this.sequence, }); AssetEditAction action; + /// Asset ID String assetId; + /// Edit ID String id; - Object parameters; + /// Edit parameters + Map parameters; + /// Edit sequence + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int sequence; @override @@ -35,7 +42,7 @@ class SyncAssetEditV1 { other.action == action && other.assetId == assetId && other.id == id && - other.parameters == parameters && + _deepEquality.equals(other.parameters, parameters) && other.sequence == sequence; @override @@ -72,7 +79,7 @@ class SyncAssetEditV1 { action: AssetEditAction.fromJson(json[r'action'])!, assetId: mapValueOfType(json, r'assetId')!, id: mapValueOfType(json, r'id')!, - parameters: mapValueOfType(json, r'parameters')!, + parameters: mapCastOfType(json, r'parameters')!, sequence: mapValueOfType(json, r'sequence')!, ); } diff --git a/mobile/openapi/lib/model/sync_asset_exif_v1.dart b/mobile/openapi/lib/model/sync_asset_exif_v1.dart index ff9efdfea3..caaeed7fb3 100644 --- a/mobile/openapi/lib/model/sync_asset_exif_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_exif_v1.dart @@ -56,9 +56,15 @@ class SyncAssetExifV1 { String? description; /// Exif image height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? exifImageHeight; /// Exif image width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? exifImageWidth; /// Exposure time @@ -68,6 +74,9 @@ class SyncAssetExifV1 { double? fNumber; /// File size in byte + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? fileSizeInByte; /// Focal length @@ -77,6 +86,9 @@ class SyncAssetExifV1 { double? fps; /// ISO + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? iso; /// Latitude @@ -107,6 +119,9 @@ class SyncAssetExifV1 { String? projectionType; /// Rating + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? rating; /// State @@ -189,7 +204,9 @@ class SyncAssetExifV1 { // json[r'country'] = null; } if (this.dateTimeOriginal != null) { - json[r'dateTimeOriginal'] = this.dateTimeOriginal!.toUtc().toIso8601String(); + json[r'dateTimeOriginal'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.dateTimeOriginal!.millisecondsSinceEpoch + : this.dateTimeOriginal!.toUtc().toIso8601String(); } else { // json[r'dateTimeOriginal'] = null; } @@ -264,7 +281,9 @@ class SyncAssetExifV1 { // json[r'model'] = null; } if (this.modifyDate != null) { - json[r'modifyDate'] = this.modifyDate!.toUtc().toIso8601String(); + json[r'modifyDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.modifyDate!.millisecondsSinceEpoch + : this.modifyDate!.toUtc().toIso8601String(); } else { // json[r'modifyDate'] = null; } @@ -313,7 +332,7 @@ class SyncAssetExifV1 { assetId: mapValueOfType(json, r'assetId')!, city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''), + dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), description: mapValueOfType(json, r'description'), exifImageHeight: mapValueOfType(json, r'exifImageHeight'), exifImageWidth: mapValueOfType(json, r'exifImageWidth'), @@ -328,7 +347,7 @@ class SyncAssetExifV1 { longitude: (mapValueOfType(json, r'longitude'))?.toDouble(), make: mapValueOfType(json, r'make'), model: mapValueOfType(json, r'model'), - modifyDate: mapDateTime(json, r'modifyDate', r''), + modifyDate: mapDateTime(json, r'modifyDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), orientation: mapValueOfType(json, r'orientation'), profileDescription: mapValueOfType(json, r'profileDescription'), projectionType: mapValueOfType(json, r'projectionType'), diff --git a/mobile/openapi/lib/model/sync_asset_face_v1.dart b/mobile/openapi/lib/model/sync_asset_face_v1.dart index 647a07d5eb..c3f74ff2cd 100644 --- a/mobile/openapi/lib/model/sync_asset_face_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_face_v1.dart @@ -28,19 +28,43 @@ class SyncAssetFaceV1 { /// Asset ID String assetId; + /// Bounding box X1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; + /// Bounding box X2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; + /// Bounding box Y1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; + /// Bounding box Y2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Asset face ID String id; + /// Image height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageHeight; + /// Image width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageWidth; /// Person ID diff --git a/mobile/openapi/lib/model/sync_asset_face_v2.dart b/mobile/openapi/lib/model/sync_asset_face_v2.dart index 688d71229f..aeefc2ece9 100644 --- a/mobile/openapi/lib/model/sync_asset_face_v2.dart +++ b/mobile/openapi/lib/model/sync_asset_face_v2.dart @@ -30,12 +30,28 @@ class SyncAssetFaceV2 { /// Asset ID String assetId; + /// Bounding box X1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; + /// Bounding box X2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; + /// Bounding box Y1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; + /// Bounding box Y2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Face deleted at @@ -44,8 +60,16 @@ class SyncAssetFaceV2 { /// Asset face ID String id; + /// Image height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageHeight; + /// Image width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageWidth; /// Is the face visible in the asset @@ -99,7 +123,9 @@ class SyncAssetFaceV2 { json[r'boundingBoxY1'] = this.boundingBoxY1; json[r'boundingBoxY2'] = this.boundingBoxY2; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } @@ -130,7 +156,7 @@ class SyncAssetFaceV2 { boundingBoxX2: mapValueOfType(json, r'boundingBoxX2')!, boundingBoxY1: mapValueOfType(json, r'boundingBoxY1')!, boundingBoxY2: mapValueOfType(json, r'boundingBoxY2')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), id: mapValueOfType(json, r'id')!, imageHeight: mapValueOfType(json, r'imageHeight')!, imageWidth: mapValueOfType(json, r'imageWidth')!, diff --git a/mobile/openapi/lib/model/sync_asset_metadata_v1.dart b/mobile/openapi/lib/model/sync_asset_metadata_v1.dart index 4a66623939..08d7eae49b 100644 --- a/mobile/openapi/lib/model/sync_asset_metadata_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_metadata_v1.dart @@ -15,7 +15,7 @@ class SyncAssetMetadataV1 { SyncAssetMetadataV1({ required this.assetId, required this.key, - required this.value, + this.value = const {}, }); /// Asset ID @@ -25,13 +25,13 @@ class SyncAssetMetadataV1 { String key; /// Value - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is SyncAssetMetadataV1 && other.assetId == assetId && other.key == key && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -62,7 +62,7 @@ class SyncAssetMetadataV1 { return SyncAssetMetadataV1( assetId: mapValueOfType(json, r'assetId')!, key: mapValueOfType(json, r'key')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_asset_v1.dart b/mobile/openapi/lib/model/sync_asset_v1.dart index debde4488e..d08de6ab72 100644 --- a/mobile/openapi/lib/model/sync_asset_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_v1.dart @@ -50,6 +50,9 @@ class SyncAssetV1 { DateTime? fileModifiedAt; /// Asset height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? height; /// Asset ID @@ -82,13 +85,14 @@ class SyncAssetV1 { /// Thumbhash String? thumbhash; - /// Asset type AssetTypeEnum type; - /// Asset visibility AssetVisibility visibility; /// Asset width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? width; @override @@ -143,7 +147,9 @@ class SyncAssetV1 { final json = {}; json[r'checksum'] = this.checksum; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } @@ -153,12 +159,16 @@ class SyncAssetV1 { // json[r'duration'] = null; } if (this.fileCreatedAt != null) { - json[r'fileCreatedAt'] = this.fileCreatedAt!.toUtc().toIso8601String(); + json[r'fileCreatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.fileCreatedAt!.millisecondsSinceEpoch + : this.fileCreatedAt!.toUtc().toIso8601String(); } else { // json[r'fileCreatedAt'] = null; } if (this.fileModifiedAt != null) { - json[r'fileModifiedAt'] = this.fileModifiedAt!.toUtc().toIso8601String(); + json[r'fileModifiedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.fileModifiedAt!.millisecondsSinceEpoch + : this.fileModifiedAt!.toUtc().toIso8601String(); } else { // json[r'fileModifiedAt'] = null; } @@ -181,7 +191,9 @@ class SyncAssetV1 { // json[r'livePhotoVideoId'] = null; } if (this.localDateTime != null) { - json[r'localDateTime'] = this.localDateTime!.toUtc().toIso8601String(); + json[r'localDateTime'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.localDateTime!.millisecondsSinceEpoch + : this.localDateTime!.toUtc().toIso8601String(); } else { // json[r'localDateTime'] = null; } @@ -217,17 +229,17 @@ class SyncAssetV1 { return SyncAssetV1( checksum: mapValueOfType(json, r'checksum')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), duration: mapValueOfType(json, r'duration'), - fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r''), - fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''), + fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), height: mapValueOfType(json, r'height'), id: mapValueOfType(json, r'id')!, isEdited: mapValueOfType(json, r'isEdited')!, isFavorite: mapValueOfType(json, r'isFavorite')!, libraryId: mapValueOfType(json, r'libraryId'), livePhotoVideoId: mapValueOfType(json, r'livePhotoVideoId'), - localDateTime: mapDateTime(json, r'localDateTime', r''), + localDateTime: mapDateTime(json, r'localDateTime', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), originalFileName: mapValueOfType(json, r'originalFileName')!, ownerId: mapValueOfType(json, r'ownerId')!, stackId: mapValueOfType(json, r'stackId'), diff --git a/mobile/openapi/lib/model/sync_auth_user_v1.dart b/mobile/openapi/lib/model/sync_auth_user_v1.dart index 0edd804c6a..c64d82bfbd 100644 --- a/mobile/openapi/lib/model/sync_auth_user_v1.dart +++ b/mobile/openapi/lib/model/sync_auth_user_v1.dart @@ -13,7 +13,7 @@ part of openapi.api; class SyncAuthUserV1 { /// Returns a new [SyncAuthUserV1] instance. SyncAuthUserV1({ - required this.avatarColor, + this.avatarColor, required this.deletedAt, required this.email, required this.hasProfileImage, @@ -28,7 +28,6 @@ class SyncAuthUserV1 { required this.storageLabel, }); - /// User avatar color UserAvatarColor? avatarColor; /// User deleted at @@ -58,8 +57,16 @@ class SyncAuthUserV1 { /// User profile changed at DateTime profileChangedAt; + /// Quota size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; + /// Quota usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int quotaUsageInBytes; /// User storage label @@ -109,7 +116,9 @@ class SyncAuthUserV1 { // json[r'avatarColor'] = null; } if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } @@ -124,7 +133,9 @@ class SyncAuthUserV1 { } else { // json[r'pinCode'] = null; } - json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); + json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.profileChangedAt.millisecondsSinceEpoch + : this.profileChangedAt.toUtc().toIso8601String(); if (this.quotaSizeInBytes != null) { json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; } else { @@ -149,7 +160,7 @@ class SyncAuthUserV1 { return SyncAuthUserV1( avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), - deletedAt: mapDateTime(json, r'deletedAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), email: mapValueOfType(json, r'email')!, hasProfileImage: mapValueOfType(json, r'hasProfileImage')!, id: mapValueOfType(json, r'id')!, @@ -157,7 +168,7 @@ class SyncAuthUserV1 { name: mapValueOfType(json, r'name')!, oauthId: mapValueOfType(json, r'oauthId')!, pinCode: mapValueOfType(json, r'pinCode'), - profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), quotaUsageInBytes: mapValueOfType(json, r'quotaUsageInBytes')!, storageLabel: mapValueOfType(json, r'storageLabel'), @@ -208,7 +219,6 @@ class SyncAuthUserV1 { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'avatarColor', 'deletedAt', 'email', 'hasProfileImage', diff --git a/mobile/openapi/lib/model/sync_memory_v1.dart b/mobile/openapi/lib/model/sync_memory_v1.dart index c506738d97..855340f4d7 100644 --- a/mobile/openapi/lib/model/sync_memory_v1.dart +++ b/mobile/openapi/lib/model/sync_memory_v1.dart @@ -14,7 +14,7 @@ class SyncMemoryV1 { /// Returns a new [SyncMemoryV1] instance. SyncMemoryV1({ required this.createdAt, - required this.data, + this.data = const {}, required this.deletedAt, required this.hideAt, required this.id, @@ -31,7 +31,7 @@ class SyncMemoryV1 { DateTime createdAt; /// Data - Object data; + Map data; /// Deleted at DateTime? deletedAt; @@ -57,7 +57,6 @@ class SyncMemoryV1 { /// Show at DateTime? showAt; - /// Memory type MemoryType type; /// Updated at @@ -66,7 +65,7 @@ class SyncMemoryV1 { @override bool operator ==(Object other) => identical(this, other) || other is SyncMemoryV1 && other.createdAt == createdAt && - other.data == data && + _deepEquality.equals(other.data, data) && other.deletedAt == deletedAt && other.hideAt == hideAt && other.id == id && @@ -99,34 +98,48 @@ class SyncMemoryV1 { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'data'] = this.data; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } if (this.hideAt != null) { - json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + json[r'hideAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.hideAt!.millisecondsSinceEpoch + : this.hideAt!.toUtc().toIso8601String(); } else { // json[r'hideAt'] = null; } json[r'id'] = this.id; json[r'isSaved'] = this.isSaved; - json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'memoryAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.memoryAt.millisecondsSinceEpoch + : this.memoryAt.toUtc().toIso8601String(); json[r'ownerId'] = this.ownerId; if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } if (this.showAt != null) { - json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + json[r'showAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.showAt!.millisecondsSinceEpoch + : this.showAt!.toUtc().toIso8601String(); } else { // json[r'showAt'] = null; } json[r'type'] = this.type; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -139,18 +152,18 @@ class SyncMemoryV1 { final json = value.cast(); return SyncMemoryV1( - createdAt: mapDateTime(json, r'createdAt', r'')!, - data: mapValueOfType(json, r'data')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), - hideAt: mapDateTime(json, r'hideAt', r''), + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + data: mapCastOfType(json, r'data')!, + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + hideAt: mapDateTime(json, r'hideAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), id: mapValueOfType(json, r'id')!, isSaved: mapValueOfType(json, r'isSaved')!, - memoryAt: mapDateTime(json, r'memoryAt', r'')!, + memoryAt: mapDateTime(json, r'memoryAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ownerId: mapValueOfType(json, r'ownerId')!, - seenAt: mapDateTime(json, r'seenAt', r''), - showAt: mapDateTime(json, r'showAt', r''), + seenAt: mapDateTime(json, r'seenAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + showAt: mapDateTime(json, r'showAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: MemoryType.fromJson(json[r'type'])!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_person_v1.dart b/mobile/openapi/lib/model/sync_person_v1.dart index fc2c36aa8c..1bd6f4a160 100644 --- a/mobile/openapi/lib/model/sync_person_v1.dart +++ b/mobile/openapi/lib/model/sync_person_v1.dart @@ -88,7 +88,9 @@ class SyncPersonV1 { Map toJson() { final json = {}; if (this.birthDate != null) { - json[r'birthDate'] = this.birthDate!.toUtc().toIso8601String(); + json[r'birthDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.birthDate!.millisecondsSinceEpoch + : this.birthDate!.toUtc().toIso8601String(); } else { // json[r'birthDate'] = null; } @@ -97,7 +99,9 @@ class SyncPersonV1 { } else { // json[r'color'] = null; } - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); if (this.faceAssetId != null) { json[r'faceAssetId'] = this.faceAssetId; } else { @@ -108,7 +112,9 @@ class SyncPersonV1 { json[r'isHidden'] = this.isHidden; json[r'name'] = this.name; json[r'ownerId'] = this.ownerId; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -121,16 +127,16 @@ class SyncPersonV1 { final json = value.cast(); return SyncPersonV1( - birthDate: mapDateTime(json, r'birthDate', r''), + birthDate: mapDateTime(json, r'birthDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), color: mapValueOfType(json, r'color'), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, faceAssetId: mapValueOfType(json, r'faceAssetId'), id: mapValueOfType(json, r'id')!, isFavorite: mapValueOfType(json, r'isFavorite')!, isHidden: mapValueOfType(json, r'isHidden')!, name: mapValueOfType(json, r'name')!, ownerId: mapValueOfType(json, r'ownerId')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_request_type.dart b/mobile/openapi/lib/model/sync_request_type.dart index 671081c0a5..f51cc8bde9 100644 --- a/mobile/openapi/lib/model/sync_request_type.dart +++ b/mobile/openapi/lib/model/sync_request_type.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Sync request types +/// Sync request type class SyncRequestType { /// Instantiate a new enum with the provided [value]. const SyncRequestType._(this.value); diff --git a/mobile/openapi/lib/model/sync_stack_v1.dart b/mobile/openapi/lib/model/sync_stack_v1.dart index e4487ccfaf..3e79a55134 100644 --- a/mobile/openapi/lib/model/sync_stack_v1.dart +++ b/mobile/openapi/lib/model/sync_stack_v1.dart @@ -57,11 +57,15 @@ class SyncStackV1 { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'ownerId'] = this.ownerId; json[r'primaryAssetId'] = this.primaryAssetId; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -74,11 +78,11 @@ class SyncStackV1 { final json = value.cast(); return SyncStackV1( - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, id: mapValueOfType(json, r'id')!, ownerId: mapValueOfType(json, r'ownerId')!, primaryAssetId: mapValueOfType(json, r'primaryAssetId')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart b/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart index 61340a8f82..67976108e1 100644 --- a/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart @@ -17,7 +17,6 @@ class SyncUserMetadataDeleteV1 { required this.userId, }); - /// User metadata key UserMetadataKey key; /// User ID diff --git a/mobile/openapi/lib/model/sync_user_metadata_v1.dart b/mobile/openapi/lib/model/sync_user_metadata_v1.dart index 23803d0be4..ddde7c0513 100644 --- a/mobile/openapi/lib/model/sync_user_metadata_v1.dart +++ b/mobile/openapi/lib/model/sync_user_metadata_v1.dart @@ -15,23 +15,22 @@ class SyncUserMetadataV1 { SyncUserMetadataV1({ required this.key, required this.userId, - required this.value, + this.value = const {}, }); - /// User metadata key UserMetadataKey key; /// User ID String userId; /// User metadata value - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is SyncUserMetadataV1 && other.key == key && other.userId == userId && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -62,7 +61,7 @@ class SyncUserMetadataV1 { return SyncUserMetadataV1( key: UserMetadataKey.fromJson(json[r'key'])!, userId: mapValueOfType(json, r'userId')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_user_v1.dart b/mobile/openapi/lib/model/sync_user_v1.dart index 6d425130a3..0a81593547 100644 --- a/mobile/openapi/lib/model/sync_user_v1.dart +++ b/mobile/openapi/lib/model/sync_user_v1.dart @@ -13,7 +13,7 @@ part of openapi.api; class SyncUserV1 { /// Returns a new [SyncUserV1] instance. SyncUserV1({ - required this.avatarColor, + this.avatarColor, required this.deletedAt, required this.email, required this.hasProfileImage, @@ -22,7 +22,6 @@ class SyncUserV1 { required this.profileChangedAt, }); - /// User avatar color UserAvatarColor? avatarColor; /// User deleted at @@ -75,7 +74,9 @@ class SyncUserV1 { // json[r'avatarColor'] = null; } if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } @@ -83,7 +84,9 @@ class SyncUserV1 { json[r'hasProfileImage'] = this.hasProfileImage; json[r'id'] = this.id; json[r'name'] = this.name; - json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); + json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.profileChangedAt.millisecondsSinceEpoch + : this.profileChangedAt.toUtc().toIso8601String(); return json; } @@ -97,12 +100,12 @@ class SyncUserV1 { return SyncUserV1( avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), - deletedAt: mapDateTime(json, r'deletedAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), email: mapValueOfType(json, r'email')!, hasProfileImage: mapValueOfType(json, r'hasProfileImage')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name')!, - profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; @@ -150,7 +153,6 @@ class SyncUserV1 { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'avatarColor', 'deletedAt', 'email', 'hasProfileImage', diff --git a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart index 6c7acbd218..ecf2e5da4a 100644 --- a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart +++ b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart @@ -36,7 +36,6 @@ class SystemConfigFFmpegDto { required this.twoPass, }); - /// Transcode hardware acceleration TranscodeHWAccel accel; /// Accelerated decode @@ -57,7 +56,6 @@ class SystemConfigFFmpegDto { /// Maximum value: 16 int bframes; - /// CQ mode CQMode cqMode; /// CRF @@ -69,6 +67,7 @@ class SystemConfigFFmpegDto { /// GOP size /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int gopSize; /// Max bitrate @@ -86,13 +85,11 @@ class SystemConfigFFmpegDto { /// Maximum value: 6 int refs; - /// Target audio codec AudioCodec targetAudioCodec; /// Target resolution String targetResolution; - /// Target video codec VideoCodec targetVideoCodec; /// Temporal AQ @@ -101,12 +98,11 @@ class SystemConfigFFmpegDto { /// Threads /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int threads; - /// Tone mapping ToneMapping tonemap; - /// Transcode policy TranscodePolicy transcode; /// Two pass diff --git a/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart b/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart index b5640f82c8..d78f8fadd5 100644 --- a/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart @@ -15,18 +15,23 @@ class SystemConfigGeneratedFullsizeImageDto { SystemConfigGeneratedFullsizeImageDto({ required this.enabled, required this.format, - this.progressive = false, + this.progressive, required this.quality, }); /// Enabled bool enabled; - /// Image format ImageFormat format; /// Progressive - bool progressive; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? progressive; /// Quality /// @@ -46,7 +51,7 @@ class SystemConfigGeneratedFullsizeImageDto { // ignore: unnecessary_parenthesis (enabled.hashCode) + (format.hashCode) + - (progressive.hashCode) + + (progressive == null ? 0 : progressive!.hashCode) + (quality.hashCode); @override @@ -56,7 +61,11 @@ class SystemConfigGeneratedFullsizeImageDto { final json = {}; json[r'enabled'] = this.enabled; json[r'format'] = this.format; + if (this.progressive != null) { json[r'progressive'] = this.progressive; + } else { + // json[r'progressive'] = null; + } json[r'quality'] = this.quality; return json; } @@ -72,7 +81,7 @@ class SystemConfigGeneratedFullsizeImageDto { return SystemConfigGeneratedFullsizeImageDto( enabled: mapValueOfType(json, r'enabled')!, format: ImageFormat.fromJson(json[r'format'])!, - progressive: mapValueOfType(json, r'progressive') ?? false, + progressive: mapValueOfType(json, r'progressive'), quality: mapValueOfType(json, r'quality')!, ); } diff --git a/mobile/openapi/lib/model/system_config_generated_image_dto.dart b/mobile/openapi/lib/model/system_config_generated_image_dto.dart index 3e8fed2c68..2571c0cab0 100644 --- a/mobile/openapi/lib/model/system_config_generated_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_generated_image_dto.dart @@ -14,15 +14,21 @@ class SystemConfigGeneratedImageDto { /// Returns a new [SystemConfigGeneratedImageDto] instance. SystemConfigGeneratedImageDto({ required this.format, - this.progressive = false, + this.progressive, required this.quality, required this.size, }); - /// Image format ImageFormat format; - bool progressive; + /// Progressive + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? progressive; /// Quality /// @@ -33,6 +39,7 @@ class SystemConfigGeneratedImageDto { /// Size /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int size; @override @@ -46,7 +53,7 @@ class SystemConfigGeneratedImageDto { int get hashCode => // ignore: unnecessary_parenthesis (format.hashCode) + - (progressive.hashCode) + + (progressive == null ? 0 : progressive!.hashCode) + (quality.hashCode) + (size.hashCode); @@ -56,7 +63,11 @@ class SystemConfigGeneratedImageDto { Map toJson() { final json = {}; json[r'format'] = this.format; + if (this.progressive != null) { json[r'progressive'] = this.progressive; + } else { + // json[r'progressive'] = null; + } json[r'quality'] = this.quality; json[r'size'] = this.size; return json; @@ -72,7 +83,7 @@ class SystemConfigGeneratedImageDto { return SystemConfigGeneratedImageDto( format: ImageFormat.fromJson(json[r'format'])!, - progressive: mapValueOfType(json, r'progressive') ?? false, + progressive: mapValueOfType(json, r'progressive'), quality: mapValueOfType(json, r'quality')!, size: mapValueOfType(json, r'size')!, ); diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart index 217a666a67..668b740872 100644 --- a/mobile/openapi/lib/model/system_config_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_image_dto.dart @@ -20,7 +20,6 @@ class SystemConfigImageDto { required this.thumbnail, }); - /// Colorspace Colorspace colorspace; /// Extract embedded diff --git a/mobile/openapi/lib/model/system_config_library_scan_dto.dart b/mobile/openapi/lib/model/system_config_library_scan_dto.dart index 28ea603c2a..003000d2ec 100644 --- a/mobile/openapi/lib/model/system_config_library_scan_dto.dart +++ b/mobile/openapi/lib/model/system_config_library_scan_dto.dart @@ -17,6 +17,7 @@ class SystemConfigLibraryScanDto { required this.enabled, }); + /// Cron expression String cronExpression; /// Enabled diff --git a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart index 2a0f1ffbc6..6162e72b8f 100644 --- a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart +++ b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart @@ -35,6 +35,7 @@ class SystemConfigMachineLearningDto { OcrConfig ocr; + /// ML service URLs List urls; @override diff --git a/mobile/openapi/lib/model/system_config_map_dto.dart b/mobile/openapi/lib/model/system_config_map_dto.dart index 109babd374..7a2fbb516b 100644 --- a/mobile/openapi/lib/model/system_config_map_dto.dart +++ b/mobile/openapi/lib/model/system_config_map_dto.dart @@ -18,11 +18,13 @@ class SystemConfigMapDto { required this.lightStyle, }); + /// Dark map style URL String darkStyle; /// Enabled bool enabled; + /// Light map style URL String lightStyle; @override diff --git a/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart b/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart index cfb18b181e..0db417427f 100644 --- a/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart +++ b/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart @@ -33,6 +33,7 @@ class SystemConfigNightlyTasksDto { /// Missing thumbnails bool missingThumbnails; + /// Start time String startTime; /// Sync quota usage diff --git a/mobile/openapi/lib/model/system_config_o_auth_dto.dart b/mobile/openapi/lib/model/system_config_o_auth_dto.dart index 82195e498b..3fd22978ff 100644 --- a/mobile/openapi/lib/model/system_config_o_auth_dto.dart +++ b/mobile/openapi/lib/model/system_config_o_auth_dto.dart @@ -13,6 +13,7 @@ part of openapi.api; class SystemConfigOAuthDto { /// Returns a new [SystemConfigOAuthDto] instance. SystemConfigOAuthDto({ + required this.allowInsecureRequests, required this.autoLaunch, required this.autoRegister, required this.buttonText, @@ -20,10 +21,12 @@ class SystemConfigOAuthDto { required this.clientSecret, required this.defaultStorageQuota, required this.enabled, + required this.endSessionEndpoint, required this.issuerUrl, required this.mobileOverrideEnabled, required this.mobileRedirectUri, required this.profileSigningAlgorithm, + required this.prompt, required this.roleClaim, required this.scope, required this.signingAlgorithm, @@ -33,6 +36,9 @@ class SystemConfigOAuthDto { required this.tokenEndpointAuthMethod, }); + /// Allow insecure requests + bool allowInsecureRequests; + /// Auto launch bool autoLaunch; @@ -51,29 +57,36 @@ class SystemConfigOAuthDto { /// Default storage quota /// /// Minimum value: 0 - int? defaultStorageQuota; + num? defaultStorageQuota; /// Enabled bool enabled; + /// End session endpoint + String endSessionEndpoint; + /// Issuer URL String issuerUrl; /// Mobile override enabled bool mobileOverrideEnabled; - /// Mobile redirect URI + /// Mobile redirect URI (set to empty string to disable) String mobileRedirectUri; /// Profile signing algorithm String profileSigningAlgorithm; + /// OAuth prompt parameter (e.g. select_account, login, consent) + String prompt; + /// Role claim String roleClaim; /// Scope String scope; + /// Signing algorithm String signingAlgorithm; /// Storage label claim @@ -85,13 +98,14 @@ class SystemConfigOAuthDto { /// Timeout /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int timeout; - /// Token endpoint auth method OAuthTokenEndpointAuthMethod tokenEndpointAuthMethod; @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigOAuthDto && + other.allowInsecureRequests == allowInsecureRequests && other.autoLaunch == autoLaunch && other.autoRegister == autoRegister && other.buttonText == buttonText && @@ -99,10 +113,12 @@ class SystemConfigOAuthDto { other.clientSecret == clientSecret && other.defaultStorageQuota == defaultStorageQuota && other.enabled == enabled && + other.endSessionEndpoint == endSessionEndpoint && other.issuerUrl == issuerUrl && other.mobileOverrideEnabled == mobileOverrideEnabled && other.mobileRedirectUri == mobileRedirectUri && other.profileSigningAlgorithm == profileSigningAlgorithm && + other.prompt == prompt && other.roleClaim == roleClaim && other.scope == scope && other.signingAlgorithm == signingAlgorithm && @@ -114,6 +130,7 @@ class SystemConfigOAuthDto { @override int get hashCode => // ignore: unnecessary_parenthesis + (allowInsecureRequests.hashCode) + (autoLaunch.hashCode) + (autoRegister.hashCode) + (buttonText.hashCode) + @@ -121,10 +138,12 @@ class SystemConfigOAuthDto { (clientSecret.hashCode) + (defaultStorageQuota == null ? 0 : defaultStorageQuota!.hashCode) + (enabled.hashCode) + + (endSessionEndpoint.hashCode) + (issuerUrl.hashCode) + (mobileOverrideEnabled.hashCode) + (mobileRedirectUri.hashCode) + (profileSigningAlgorithm.hashCode) + + (prompt.hashCode) + (roleClaim.hashCode) + (scope.hashCode) + (signingAlgorithm.hashCode) + @@ -134,10 +153,11 @@ class SystemConfigOAuthDto { (tokenEndpointAuthMethod.hashCode); @override - String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, roleClaim=$roleClaim, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]'; + String toString() => 'SystemConfigOAuthDto[allowInsecureRequests=$allowInsecureRequests, autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, endSessionEndpoint=$endSessionEndpoint, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, prompt=$prompt, roleClaim=$roleClaim, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]'; Map toJson() { final json = {}; + json[r'allowInsecureRequests'] = this.allowInsecureRequests; json[r'autoLaunch'] = this.autoLaunch; json[r'autoRegister'] = this.autoRegister; json[r'buttonText'] = this.buttonText; @@ -149,10 +169,12 @@ class SystemConfigOAuthDto { // json[r'defaultStorageQuota'] = null; } json[r'enabled'] = this.enabled; + json[r'endSessionEndpoint'] = this.endSessionEndpoint; json[r'issuerUrl'] = this.issuerUrl; json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled; json[r'mobileRedirectUri'] = this.mobileRedirectUri; json[r'profileSigningAlgorithm'] = this.profileSigningAlgorithm; + json[r'prompt'] = this.prompt; json[r'roleClaim'] = this.roleClaim; json[r'scope'] = this.scope; json[r'signingAlgorithm'] = this.signingAlgorithm; @@ -172,17 +194,22 @@ class SystemConfigOAuthDto { final json = value.cast(); return SystemConfigOAuthDto( + allowInsecureRequests: mapValueOfType(json, r'allowInsecureRequests')!, autoLaunch: mapValueOfType(json, r'autoLaunch')!, autoRegister: mapValueOfType(json, r'autoRegister')!, buttonText: mapValueOfType(json, r'buttonText')!, clientId: mapValueOfType(json, r'clientId')!, clientSecret: mapValueOfType(json, r'clientSecret')!, - defaultStorageQuota: mapValueOfType(json, r'defaultStorageQuota'), + defaultStorageQuota: json[r'defaultStorageQuota'] == null + ? null + : num.parse('${json[r'defaultStorageQuota']}'), enabled: mapValueOfType(json, r'enabled')!, + endSessionEndpoint: mapValueOfType(json, r'endSessionEndpoint')!, issuerUrl: mapValueOfType(json, r'issuerUrl')!, mobileOverrideEnabled: mapValueOfType(json, r'mobileOverrideEnabled')!, mobileRedirectUri: mapValueOfType(json, r'mobileRedirectUri')!, profileSigningAlgorithm: mapValueOfType(json, r'profileSigningAlgorithm')!, + prompt: mapValueOfType(json, r'prompt')!, roleClaim: mapValueOfType(json, r'roleClaim')!, scope: mapValueOfType(json, r'scope')!, signingAlgorithm: mapValueOfType(json, r'signingAlgorithm')!, @@ -237,6 +264,7 @@ class SystemConfigOAuthDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'allowInsecureRequests', 'autoLaunch', 'autoRegister', 'buttonText', @@ -244,10 +272,12 @@ class SystemConfigOAuthDto { 'clientSecret', 'defaultStorageQuota', 'enabled', + 'endSessionEndpoint', 'issuerUrl', 'mobileOverrideEnabled', 'mobileRedirectUri', 'profileSigningAlgorithm', + 'prompt', 'roleClaim', 'scope', 'signingAlgorithm', diff --git a/mobile/openapi/lib/model/system_config_template_emails_dto.dart b/mobile/openapi/lib/model/system_config_template_emails_dto.dart index 9db85509f5..d29ca1fac3 100644 --- a/mobile/openapi/lib/model/system_config_template_emails_dto.dart +++ b/mobile/openapi/lib/model/system_config_template_emails_dto.dart @@ -18,10 +18,13 @@ class SystemConfigTemplateEmailsDto { required this.welcomeTemplate, }); + /// Album invite template String albumInviteTemplate; + /// Album update template String albumUpdateTemplate; + /// Welcome template String welcomeTemplate; @override diff --git a/mobile/openapi/lib/model/system_config_trash_dto.dart b/mobile/openapi/lib/model/system_config_trash_dto.dart index 9bdaef92d3..790710751f 100644 --- a/mobile/openapi/lib/model/system_config_trash_dto.dart +++ b/mobile/openapi/lib/model/system_config_trash_dto.dart @@ -20,6 +20,7 @@ class SystemConfigTrashDto { /// Days /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int days; /// Enabled diff --git a/mobile/openapi/lib/model/system_config_user_dto.dart b/mobile/openapi/lib/model/system_config_user_dto.dart index a7313560e6..dc553e7369 100644 --- a/mobile/openapi/lib/model/system_config_user_dto.dart +++ b/mobile/openapi/lib/model/system_config_user_dto.dart @@ -19,6 +19,7 @@ class SystemConfigUserDto { /// Delete delay /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int deleteDelay; @override diff --git a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart index 5566846e3c..4d689f01a1 100644 --- a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart +++ b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart @@ -17,6 +17,9 @@ class TagBulkAssetsResponseDto { }); /// Number of assets tagged + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int count; @override diff --git a/mobile/openapi/lib/model/tag_create_dto.dart b/mobile/openapi/lib/model/tag_create_dto.dart index fd6a10163c..e05b29f1ed 100644 --- a/mobile/openapi/lib/model/tag_create_dto.dart +++ b/mobile/openapi/lib/model/tag_create_dto.dart @@ -19,12 +19,6 @@ class TagCreateDto { }); /// Tag color (hex) - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? color; /// Tag name diff --git a/mobile/openapi/lib/model/tags_response.dart b/mobile/openapi/lib/model/tags_response.dart index 1e4a4bd109..8a3ac17474 100644 --- a/mobile/openapi/lib/model/tags_response.dart +++ b/mobile/openapi/lib/model/tags_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class TagsResponse { /// Returns a new [TagsResponse] instance. TagsResponse({ - this.enabled = true, - this.sidebarWeb = true, + required this.enabled, + required this.sidebarWeb, }); /// Whether tags are enabled diff --git a/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart b/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart index 720323cd14..e2f9bec1ec 100644 --- a/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart +++ b/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart @@ -39,7 +39,7 @@ class TimeBucketAssetResponseDto { /// Array of country names extracted from EXIF GPS data List country; - /// Array of video durations in HH:MM:SS format (null for images) + /// Array of video/gif durations in hh:mm:ss.SSS format (null for static images) List duration; /// Array of file creation timestamps in UTC diff --git a/mobile/openapi/lib/model/time_buckets_response_dto.dart b/mobile/openapi/lib/model/time_buckets_response_dto.dart index 11faa815e2..8b8da1d37a 100644 --- a/mobile/openapi/lib/model/time_buckets_response_dto.dart +++ b/mobile/openapi/lib/model/time_buckets_response_dto.dart @@ -18,6 +18,9 @@ class TimeBucketsResponseDto { }); /// Number of assets in this time bucket + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int count; /// Time bucket identifier in YYYY-MM-DD format representing the start of the time period diff --git a/mobile/openapi/lib/model/trash_response_dto.dart b/mobile/openapi/lib/model/trash_response_dto.dart index 7edd5d032a..7b43d9ceb7 100644 --- a/mobile/openapi/lib/model/trash_response_dto.dart +++ b/mobile/openapi/lib/model/trash_response_dto.dart @@ -17,6 +17,9 @@ class TrashResponseDto { }); /// Number of items in trash + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int count; @override diff --git a/mobile/openapi/lib/model/update_album_dto.dart b/mobile/openapi/lib/model/update_album_dto.dart index 46ce8b0ecc..ae4a5c1f87 100644 --- a/mobile/openapi/lib/model/update_album_dto.dart +++ b/mobile/openapi/lib/model/update_album_dto.dart @@ -56,7 +56,6 @@ class UpdateAlbumDto { /// bool? isActivityEnabled; - /// Asset sort order /// /// 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 diff --git a/mobile/openapi/lib/model/update_album_user_dto.dart b/mobile/openapi/lib/model/update_album_user_dto.dart index 9d934eb465..43218cae6e 100644 --- a/mobile/openapi/lib/model/update_album_user_dto.dart +++ b/mobile/openapi/lib/model/update_album_user_dto.dart @@ -16,7 +16,6 @@ class UpdateAlbumUserDto { required this.role, }); - /// Album user role AlbumUserRole role; @override diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart index 8526995934..2c4c3352ea 100644 --- a/mobile/openapi/lib/model/update_asset_dto.dart +++ b/mobile/openapi/lib/model/update_asset_dto.dart @@ -52,6 +52,9 @@ class UpdateAssetDto { /// Latitude coordinate /// + /// Minimum value: -90 + /// Maximum value: 90 + /// /// 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. @@ -64,6 +67,9 @@ class UpdateAssetDto { /// Longitude coordinate /// + /// Minimum value: -180 + /// Maximum value: 180 + /// /// 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. @@ -75,9 +81,8 @@ class UpdateAssetDto { /// /// Minimum value: -1 /// Maximum value: 5 - num? rating; + int? rating; - /// Asset visibility /// /// 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 @@ -172,9 +177,7 @@ class UpdateAssetDto { latitude: num.parse('${json[r'latitude']}'), livePhotoVideoId: mapValueOfType(json, r'livePhotoVideoId'), longitude: num.parse('${json[r'longitude']}'), - rating: json[r'rating'] == null - ? null - : num.parse('${json[r'rating']}'), + rating: mapValueOfType(json, r'rating'), visibility: AssetVisibility.fromJson(json[r'visibility']), ); } diff --git a/mobile/openapi/lib/model/update_library_dto.dart b/mobile/openapi/lib/model/update_library_dto.dart index 628bdc0055..276d43ecd9 100644 --- a/mobile/openapi/lib/model/update_library_dto.dart +++ b/mobile/openapi/lib/model/update_library_dto.dart @@ -13,16 +13,16 @@ part of openapi.api; class UpdateLibraryDto { /// Returns a new [UpdateLibraryDto] instance. UpdateLibraryDto({ - this.exclusionPatterns = const {}, - this.importPaths = const {}, + this.exclusionPatterns = const [], + this.importPaths = const [], this.name, }); /// Exclusion patterns (max 128) - Set exclusionPatterns; + List exclusionPatterns; /// Import paths (max 128) - Set importPaths; + List importPaths; /// Library name /// @@ -51,8 +51,8 @@ class UpdateLibraryDto { Map toJson() { final json = {}; - json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false); - json[r'importPaths'] = this.importPaths.toList(growable: false); + json[r'exclusionPatterns'] = this.exclusionPatterns; + json[r'importPaths'] = this.importPaths; if (this.name != null) { json[r'name'] = this.name; } else { @@ -71,11 +71,11 @@ class UpdateLibraryDto { return UpdateLibraryDto( exclusionPatterns: json[r'exclusionPatterns'] is Iterable - ? (json[r'exclusionPatterns'] as Iterable).cast().toSet() - : const {}, + ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) + : const [], importPaths: json[r'importPaths'] is Iterable - ? (json[r'importPaths'] as Iterable).cast().toSet() - : const {}, + ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) + : const [], name: mapValueOfType(json, r'name'), ); } diff --git a/mobile/openapi/lib/model/usage_by_user_dto.dart b/mobile/openapi/lib/model/usage_by_user_dto.dart index da1fe600a5..462b82c3e0 100644 --- a/mobile/openapi/lib/model/usage_by_user_dto.dart +++ b/mobile/openapi/lib/model/usage_by_user_dto.dart @@ -24,18 +24,33 @@ class UsageByUserDto { }); /// Number of photos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int photos; /// User quota size in bytes (null if unlimited) + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Total storage usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usage; /// Storage usage for photos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usagePhotos; /// Storage usage for videos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usageVideos; /// User ID @@ -45,6 +60,9 @@ class UsageByUserDto { String userName; /// Number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/user_admin_create_dto.dart b/mobile/openapi/lib/model/user_admin_create_dto.dart index 485b2e00e5..54da0b0566 100644 --- a/mobile/openapi/lib/model/user_admin_create_dto.dart +++ b/mobile/openapi/lib/model/user_admin_create_dto.dart @@ -25,7 +25,6 @@ class UserAdminCreateDto { this.storageLabel, }); - /// Avatar color UserAvatarColor? avatarColor; /// User email @@ -61,6 +60,7 @@ class UserAdminCreateDto { /// Storage quota in bytes /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Require password change on next login diff --git a/mobile/openapi/lib/model/user_admin_response_dto.dart b/mobile/openapi/lib/model/user_admin_response_dto.dart index 706f65cf35..09f8cedce4 100644 --- a/mobile/openapi/lib/model/user_admin_response_dto.dart +++ b/mobile/openapi/lib/model/user_admin_response_dto.dart @@ -32,7 +32,6 @@ class UserAdminResponseDto { required this.updatedAt, }); - /// Avatar color UserAvatarColor avatarColor; /// Creation date @@ -50,7 +49,6 @@ class UserAdminResponseDto { /// Is admin user bool isAdmin; - /// User license UserLicense? license; /// User name @@ -66,15 +64,20 @@ class UserAdminResponseDto { String profileImagePath; /// Storage quota in bytes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Storage usage in bytes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaUsageInBytes; /// Require password change on next login bool shouldChangePassword; - /// User status UserStatus status; /// Storage label @@ -130,9 +133,13 @@ class UserAdminResponseDto { Map toJson() { final json = {}; json[r'avatarColor'] = this.avatarColor; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } @@ -165,7 +172,9 @@ class UserAdminResponseDto { } else { // json[r'storageLabel'] = null; } - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -179,8 +188,8 @@ class UserAdminResponseDto { return UserAdminResponseDto( avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!, - createdAt: mapDateTime(json, r'createdAt', r'')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), email: mapValueOfType(json, r'email')!, id: mapValueOfType(json, r'id')!, isAdmin: mapValueOfType(json, r'isAdmin')!, @@ -194,7 +203,7 @@ class UserAdminResponseDto { shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, status: UserStatus.fromJson(json[r'status'])!, storageLabel: mapValueOfType(json, r'storageLabel'), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/user_admin_update_dto.dart b/mobile/openapi/lib/model/user_admin_update_dto.dart index 3cce65745f..0c33a46139 100644 --- a/mobile/openapi/lib/model/user_admin_update_dto.dart +++ b/mobile/openapi/lib/model/user_admin_update_dto.dart @@ -24,7 +24,6 @@ class UserAdminUpdateDto { this.storageLabel, }); - /// Avatar color UserAvatarColor? avatarColor; /// User email @@ -69,6 +68,7 @@ class UserAdminUpdateDto { /// Storage quota in bytes /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Require password change on next login diff --git a/mobile/openapi/lib/model/user_avatar_color.dart b/mobile/openapi/lib/model/user_avatar_color.dart index 4fcf518550..719e366899 100644 --- a/mobile/openapi/lib/model/user_avatar_color.dart +++ b/mobile/openapi/lib/model/user_avatar_color.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Avatar color +/// User avatar color class UserAvatarColor { /// Instantiate a new enum with the provided [value]. const UserAvatarColor._(this.value); diff --git a/mobile/openapi/lib/model/user_license.dart b/mobile/openapi/lib/model/user_license.dart index f02dc73bef..8ef46a0bb5 100644 --- a/mobile/openapi/lib/model/user_license.dart +++ b/mobile/openapi/lib/model/user_license.dart @@ -24,7 +24,7 @@ class UserLicense { /// Activation key String activationKey; - /// License key + /// License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/) String licenseKey; @override @@ -45,7 +45,9 @@ class UserLicense { Map toJson() { final json = {}; - json[r'activatedAt'] = this.activatedAt.toUtc().toIso8601String(); + json[r'activatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.activatedAt.millisecondsSinceEpoch + : this.activatedAt.toUtc().toIso8601String(); json[r'activationKey'] = this.activationKey; json[r'licenseKey'] = this.licenseKey; return json; @@ -60,7 +62,7 @@ class UserLicense { final json = value.cast(); return UserLicense( - activatedAt: mapDateTime(json, r'activatedAt', r'')!, + activatedAt: mapDateTime(json, r'activatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, activationKey: mapValueOfType(json, r'activationKey')!, licenseKey: mapValueOfType(json, r'licenseKey')!, ); diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index bf0e2cbf09..f671072c72 100644 --- a/mobile/openapi/lib/model/user_response_dto.dart +++ b/mobile/openapi/lib/model/user_response_dto.dart @@ -21,7 +21,6 @@ class UserResponseDto { required this.profileImagePath, }); - /// Avatar color UserAvatarColor avatarColor; /// User email diff --git a/mobile/openapi/lib/model/user_update_me_dto.dart b/mobile/openapi/lib/model/user_update_me_dto.dart index 066c435eb3..0751d4096b 100644 --- a/mobile/openapi/lib/model/user_update_me_dto.dart +++ b/mobile/openapi/lib/model/user_update_me_dto.dart @@ -19,7 +19,6 @@ class UserUpdateMeDto { this.password, }); - /// Avatar color UserAvatarColor? avatarColor; /// User email diff --git a/mobile/openapi/lib/model/validate_library_dto.dart b/mobile/openapi/lib/model/validate_library_dto.dart index 59c3680782..68fb0e9fe2 100644 --- a/mobile/openapi/lib/model/validate_library_dto.dart +++ b/mobile/openapi/lib/model/validate_library_dto.dart @@ -13,15 +13,15 @@ part of openapi.api; class ValidateLibraryDto { /// Returns a new [ValidateLibraryDto] instance. ValidateLibraryDto({ - this.exclusionPatterns = const {}, - this.importPaths = const {}, + this.exclusionPatterns = const [], + this.importPaths = const [], }); /// Exclusion patterns (max 128) - Set exclusionPatterns; + List exclusionPatterns; /// Import paths to validate (max 128) - Set importPaths; + List importPaths; @override bool operator ==(Object other) => identical(this, other) || other is ValidateLibraryDto && @@ -39,8 +39,8 @@ class ValidateLibraryDto { Map toJson() { final json = {}; - json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false); - json[r'importPaths'] = this.importPaths.toList(growable: false); + json[r'exclusionPatterns'] = this.exclusionPatterns; + json[r'importPaths'] = this.importPaths; return json; } @@ -54,11 +54,11 @@ class ValidateLibraryDto { return ValidateLibraryDto( exclusionPatterns: json[r'exclusionPatterns'] is Iterable - ? (json[r'exclusionPatterns'] as Iterable).cast().toSet() - : const {}, + ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) + : const [], importPaths: json[r'importPaths'] is Iterable - ? (json[r'importPaths'] as Iterable).cast().toSet() - : const {}, + ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) + : const [], ); } return null; diff --git a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart index 78cc03dc94..ebcb881935 100644 --- a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart +++ b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart @@ -14,7 +14,7 @@ class ValidateLibraryImportPathResponseDto { /// Returns a new [ValidateLibraryImportPathResponseDto] instance. ValidateLibraryImportPathResponseDto({ required this.importPath, - this.isValid = false, + required this.isValid, this.message, }); diff --git a/mobile/openapi/lib/model/video_container.dart b/mobile/openapi/lib/model/video_container.dart index b1a47c8721..a291fabf6e 100644 --- a/mobile/openapi/lib/model/video_container.dart +++ b/mobile/openapi/lib/model/video_container.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Accepted containers +/// Accepted video containers class VideoContainer { /// Instantiate a new enum with the provided [value]. const VideoContainer._(this.value); diff --git a/mobile/openapi/lib/model/workflow_action_item_dto.dart b/mobile/openapi/lib/model/workflow_action_item_dto.dart index 9222dd6ba7..1ad70238d8 100644 --- a/mobile/openapi/lib/model/workflow_action_item_dto.dart +++ b/mobile/openapi/lib/model/workflow_action_item_dto.dart @@ -13,31 +13,24 @@ part of openapi.api; class WorkflowActionItemDto { /// Returns a new [WorkflowActionItemDto] instance. WorkflowActionItemDto({ - this.actionConfig, + this.actionConfig = const {}, required this.pluginActionId, }); - /// Action configuration - /// - /// 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. - /// - Object? actionConfig; + Map actionConfig; /// Plugin action ID String pluginActionId; @override bool operator ==(Object other) => identical(this, other) || other is WorkflowActionItemDto && - other.actionConfig == actionConfig && + _deepEquality.equals(other.actionConfig, actionConfig) && other.pluginActionId == pluginActionId; @override int get hashCode => // ignore: unnecessary_parenthesis - (actionConfig == null ? 0 : actionConfig!.hashCode) + + (actionConfig.hashCode) + (pluginActionId.hashCode); @override @@ -45,11 +38,7 @@ class WorkflowActionItemDto { Map toJson() { final json = {}; - if (this.actionConfig != null) { json[r'actionConfig'] = this.actionConfig; - } else { - // json[r'actionConfig'] = null; - } json[r'pluginActionId'] = this.pluginActionId; return json; } @@ -63,7 +52,7 @@ class WorkflowActionItemDto { final json = value.cast(); return WorkflowActionItemDto( - actionConfig: mapValueOfType(json, r'actionConfig'), + actionConfig: mapCastOfType(json, r'actionConfig') ?? const {}, pluginActionId: mapValueOfType(json, r'pluginActionId')!, ); } diff --git a/mobile/openapi/lib/model/workflow_action_response_dto.dart b/mobile/openapi/lib/model/workflow_action_response_dto.dart index 8f77e9cf2b..dcbb5ee8ef 100644 --- a/mobile/openapi/lib/model/workflow_action_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_action_response_dto.dart @@ -20,8 +20,7 @@ class WorkflowActionResponseDto { required this.workflowId, }); - /// Action configuration - Object? actionConfig; + Map? actionConfig; /// Action ID String id; @@ -37,7 +36,7 @@ class WorkflowActionResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is WorkflowActionResponseDto && - other.actionConfig == actionConfig && + _deepEquality.equals(other.actionConfig, actionConfig) && other.id == id && other.order == order && other.pluginActionId == pluginActionId && @@ -78,7 +77,7 @@ class WorkflowActionResponseDto { final json = value.cast(); return WorkflowActionResponseDto( - actionConfig: mapValueOfType(json, r'actionConfig'), + actionConfig: mapCastOfType(json, r'actionConfig'), id: mapValueOfType(json, r'id')!, order: num.parse('${json[r'order']}'), pluginActionId: mapValueOfType(json, r'pluginActionId')!, diff --git a/mobile/openapi/lib/model/workflow_create_dto.dart b/mobile/openapi/lib/model/workflow_create_dto.dart index 38665a1912..143af0ca6c 100644 --- a/mobile/openapi/lib/model/workflow_create_dto.dart +++ b/mobile/openapi/lib/model/workflow_create_dto.dart @@ -48,7 +48,6 @@ class WorkflowCreateDto { /// Workflow name String name; - /// Workflow trigger type PluginTriggerType triggerType; @override diff --git a/mobile/openapi/lib/model/workflow_filter_item_dto.dart b/mobile/openapi/lib/model/workflow_filter_item_dto.dart index 52e29c3e93..92224b9f16 100644 --- a/mobile/openapi/lib/model/workflow_filter_item_dto.dart +++ b/mobile/openapi/lib/model/workflow_filter_item_dto.dart @@ -13,31 +13,24 @@ part of openapi.api; class WorkflowFilterItemDto { /// Returns a new [WorkflowFilterItemDto] instance. WorkflowFilterItemDto({ - this.filterConfig, + this.filterConfig = const {}, required this.pluginFilterId, }); - /// Filter configuration - /// - /// 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. - /// - Object? filterConfig; + Map filterConfig; /// Plugin filter ID String pluginFilterId; @override bool operator ==(Object other) => identical(this, other) || other is WorkflowFilterItemDto && - other.filterConfig == filterConfig && + _deepEquality.equals(other.filterConfig, filterConfig) && other.pluginFilterId == pluginFilterId; @override int get hashCode => // ignore: unnecessary_parenthesis - (filterConfig == null ? 0 : filterConfig!.hashCode) + + (filterConfig.hashCode) + (pluginFilterId.hashCode); @override @@ -45,11 +38,7 @@ class WorkflowFilterItemDto { Map toJson() { final json = {}; - if (this.filterConfig != null) { json[r'filterConfig'] = this.filterConfig; - } else { - // json[r'filterConfig'] = null; - } json[r'pluginFilterId'] = this.pluginFilterId; return json; } @@ -63,7 +52,7 @@ class WorkflowFilterItemDto { final json = value.cast(); return WorkflowFilterItemDto( - filterConfig: mapValueOfType(json, r'filterConfig'), + filterConfig: mapCastOfType(json, r'filterConfig') ?? const {}, pluginFilterId: mapValueOfType(json, r'pluginFilterId')!, ); } diff --git a/mobile/openapi/lib/model/workflow_filter_response_dto.dart b/mobile/openapi/lib/model/workflow_filter_response_dto.dart index 355378adac..932722f5a5 100644 --- a/mobile/openapi/lib/model/workflow_filter_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_filter_response_dto.dart @@ -20,8 +20,7 @@ class WorkflowFilterResponseDto { required this.workflowId, }); - /// Filter configuration - Object? filterConfig; + Map? filterConfig; /// Filter ID String id; @@ -37,7 +36,7 @@ class WorkflowFilterResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is WorkflowFilterResponseDto && - other.filterConfig == filterConfig && + _deepEquality.equals(other.filterConfig, filterConfig) && other.id == id && other.order == order && other.pluginFilterId == pluginFilterId && @@ -78,7 +77,7 @@ class WorkflowFilterResponseDto { final json = value.cast(); return WorkflowFilterResponseDto( - filterConfig: mapValueOfType(json, r'filterConfig'), + filterConfig: mapCastOfType(json, r'filterConfig'), id: mapValueOfType(json, r'id')!, order: num.parse('${json[r'order']}'), pluginFilterId: mapValueOfType(json, r'pluginFilterId')!, diff --git a/mobile/openapi/lib/model/workflow_response_dto.dart b/mobile/openapi/lib/model/workflow_response_dto.dart index ae3e6510aa..6461b62508 100644 --- a/mobile/openapi/lib/model/workflow_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_response_dto.dart @@ -48,7 +48,6 @@ class WorkflowResponseDto { /// Owner user ID String ownerId; - /// Workflow trigger type PluginTriggerType triggerType; @override diff --git a/mobile/openapi/lib/model/workflow_update_dto.dart b/mobile/openapi/lib/model/workflow_update_dto.dart index 9891fff079..9abb45ddd5 100644 --- a/mobile/openapi/lib/model/workflow_update_dto.dart +++ b/mobile/openapi/lib/model/workflow_update_dto.dart @@ -54,7 +54,6 @@ class WorkflowUpdateDto { /// String? name; - /// Workflow trigger type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/packages/ui/pubspec.lock b/mobile/packages/ui/pubspec.lock index 697e1debf5..4ac863d0f7 100644 --- a/mobile/packages/ui/pubspec.lock +++ b/mobile/packages/ui/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -87,26 +87,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" path: dependency: transitive description: @@ -164,10 +164,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.10" vector_math: dependency: transitive description: @@ -185,5 +185,5 @@ packages: source: hosted version: "15.0.2" sdks: - dart: ">=3.8.0-0 <4.0.0" + dart: ">=3.11.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/mobile/packages/ui/pubspec.yaml b/mobile/packages/ui/pubspec.yaml index a25dfb6ca4..de50e0a429 100644 --- a/mobile/packages/ui/pubspec.yaml +++ b/mobile/packages/ui/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_ui publish_to: none environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.11.0 <4.0.0' dependencies: flutter: diff --git a/mobile/packages/ui/showcase/pubspec.lock b/mobile/packages/ui/showcase/pubspec.lock index c79e6c18c7..c676b23c53 100644 --- a/mobile/packages/ui/showcase/pubspec.lock +++ b/mobile/packages/ui/showcase/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -124,10 +124,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: eff94d2a6fc79fa8b811dde79c7549808c2346037ee107a1121b4a644c745f2a + sha256: "5540e4a3f416dd4a93458257b908eb88353cbd0fb5b0a3d1bd7d849ba1e88735" url: "https://pub.dev" source: hosted - version: "17.0.1" + version: "17.2.1" immich_ui: dependency: "direct main" description: @@ -195,26 +195,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" path: dependency: transitive description: @@ -312,10 +312,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.10" typed_data: dependency: transitive description: @@ -373,5 +373,5 @@ packages: source: hosted version: "2.1.0" sdks: - dart: ">=3.9.2 <4.0.0" + dart: ">=3.11.0 <4.0.0" flutter: ">=3.35.0" diff --git a/mobile/packages/ui/showcase/pubspec.yaml b/mobile/packages/ui/showcase/pubspec.yaml index e45ce07e66..6353600ce3 100644 --- a/mobile/packages/ui/showcase/pubspec.yaml +++ b/mobile/packages/ui/showcase/pubspec.yaml @@ -4,14 +4,14 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ^3.9.2 + sdk: ^3.11.0 dependencies: flutter: sdk: flutter immich_ui: path: ../ - go_router: ^17.0.1 + go_router: ^17.2.1 syntax_highlight: ^0.5.0 dev_dependencies: diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 89a43f328b..e0e3c4ddc8 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -5,26 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 + sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" url: "https://pub.dev" source: hosted - version: "80.0.0" + version: "93.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" + sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b url: "https://pub.dev" source: hosted - version: "7.3.0" - analyzer_plugin: - dependency: transitive - description: - name: analyzer_plugin - sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4 - url: "https://pub.dev" - source: hosted - version: "0.13.0" + version: "10.0.1" ansicolor: dependency: transitive description: @@ -37,10 +29,10 @@ packages: dependency: transitive description: name: archive - sha256: "0c64e928dcbefddecd234205422bcfc2b5e6d31be0b86fef0d0dd48d7b4c9742" + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.0.9" args: dependency: transitive description: @@ -53,36 +45,36 @@ packages: dependency: "direct main" description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.13.1" auto_route: dependency: "direct main" description: name: auto_route - sha256: "1d1bd908a1fec327719326d5d0791edd37f16caff6493c01003689fb03315ad7" + sha256: e9acfeb3df33d188fce4ad0239ef4238f333b7aa4d95ec52af3c2b9360dcd969 url: "https://pub.dev" source: hosted - version: "9.3.0+1" + version: "11.1.0" auto_route_generator: dependency: "direct dev" description: name: auto_route_generator - sha256: c2e359d8932986d4d1bcad7a428143f81384ce10fef8d4aa5bc29e1f83766a46 + sha256: "7aa0e90874928e78709f0a21a69fb5bc2ae1aa932dec862930d2af85c40adb01" url: "https://pub.dev" source: hosted - version: "9.3.1" + version: "10.5.0" background_downloader: dependency: "direct main" description: name: background_downloader - sha256: a913b37cc47a656a225e9562b69576000d516f705482f392e2663500e6ff6032 + sha256: "4cb23d9ad4f5060944f38164e7b90d4bf99b57b2472a3bd4676e59b2db4afd06" url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.5.4" bonsoir: - dependency: transitive + dependency: "direct overridden" description: name: bonsoir sha256: "2e2cf3be580deccad9a48dcaddddf90de092e74b7de2015ef58fb24e11d66496" @@ -141,50 +133,34 @@ packages: dependency: transitive description: name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "4.0.5" build_config: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.3.0" build_daemon: dependency: transitive description: name: build_daemon - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 url: "https://pub.dev" source: hosted - version: "4.0.4" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 - url: "https://pub.dev" - source: hosted - version: "2.4.4" + version: "4.1.1" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" + sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e" url: "https://pub.dev" source: hosted - version: "2.4.15" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" - url: "https://pub.dev" - source: hosted - version: "8.0.0" + version: "2.13.1" built_collection: dependency: transitive description: @@ -197,10 +173,10 @@ packages: dependency: transitive description: name: built_value - sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af" url: "https://pub.dev" source: hosted - version: "8.9.5" + version: "8.12.5" cast: dependency: "direct main" description: @@ -213,10 +189,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -229,18 +205,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" - ci: - dependency: transitive - description: - name: ci - sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" - url: "https://pub.dev" - source: hosted - version: "0.1.0" + version: "2.0.4" cli_util: dependency: transitive description: @@ -257,14 +225,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" code_builder: dependency: transitive description: name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" url: "https://pub.dev" source: hosted - version: "4.10.1" + version: "4.11.1" collection: dependency: "direct main" description: @@ -285,10 +261,10 @@ packages: dependency: transitive description: name: connectivity_plus_platform_interface - sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" convert: dependency: transitive description: @@ -301,26 +277,26 @@ packages: dependency: "direct main" description: name: crop_image - sha256: "4fdebd00d0c7d1a6e3abeb1e3843efbc202204b867f3e377fcebcf77aaf69a17" + sha256: "27cbce1685a595efee62caab81c98b49b636f765c1da86353f58f5b2bf2775d8" url: "https://pub.dev" source: hosted - version: "1.0.16" + version: "1.0.17" cross_file: dependency: transitive description: name: cross_file - sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" url: "https://pub.dev" source: hosted - version: "0.3.4+2" + version: "0.3.5+2" crypto: dependency: "direct main" description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" csslib: dependency: transitive description: @@ -338,62 +314,22 @@ packages: url: "https://github.com/mertalev/http" source: git version: "3.0.0-wip" - custom_lint: - dependency: "direct dev" - description: - name: custom_lint - sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1" - url: "https://pub.dev" - source: hosted - version: "0.7.5" - custom_lint_builder: - dependency: transitive - description: - name: custom_lint_builder - sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228" - url: "https://pub.dev" - source: hosted - version: "0.7.5" - custom_lint_core: - dependency: transitive - description: - name: custom_lint_core - sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" - url: "https://pub.dev" - source: hosted - version: "0.7.5" - custom_lint_visitor: - dependency: transitive - description: - name: custom_lint_visitor - sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8" - url: "https://pub.dev" - source: hosted - version: "1.0.0+7.3.0" dart_style: dependency: transitive description: name: dart_style - sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" + sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2" url: "https://pub.dev" source: hosted - version: "3.1.0" - dartx: - dependency: transitive - description: - name: dartx - sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" - url: "https://pub.dev" - source: hosted - version: "1.2.0" + version: "3.1.7" dbus: dependency: transitive description: name: dbus - sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" desktop_webview_window: dependency: transitive description: @@ -406,10 +342,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: dd0e8e02186b2196c7848c9d394a5fd6e5b57a43a546082c5820b1ec72317e33 + sha256: b4fed1b2835da9d670d7bed7db79ae2a94b0f5ad6312268158a9b5479abbacdd url: "https://pub.dev" source: hosted - version: "12.2.0" + version: "12.4.0" device_info_plus_platform_interface: dependency: transitive description: @@ -421,28 +357,27 @@ packages: drift: dependency: "direct main" description: - path: drift - ref: "53ef7e9f19fe8f68416251760b4b99fe43f1c575" - resolved-ref: "53ef7e9f19fe8f68416251760b4b99fe43f1c575" - url: "https://github.com/immich-app/drift" - source: git - version: "2.26.0" + name: drift + sha256: "055c249d1f91be5a47fe447f88afc24c4ca6f4cd6c5ed66767b4797d48acc2e5" + url: "https://pub.dev" + source: hosted + version: "2.32.1" drift_dev: dependency: "direct dev" description: name: drift_dev - sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588" + sha256: "88a9de3af8571518148a6d8a513b57779fd1e60a026d3ab8a481a878fba01d91" url: "https://pub.dev" source: hosted - version: "2.26.0" + version: "2.32.1" drift_flutter: dependency: "direct main" description: name: drift_flutter - sha256: b52bd710f809db11e25259d429d799d034ba1c5224ce6a73fe8419feb980d44c + sha256: "887fdec622174dc7eaefd0048403e34ee07cc18626ac8a7544cc3b8a4a172166" url: "https://pub.dev" source: hosted - version: "0.2.6" + version: "0.3.0" dynamic_color: dependency: "direct main" description: @@ -479,10 +414,10 @@ packages: dependency: "direct main" description: name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" file: dependency: "direct dev" description: @@ -495,34 +430,34 @@ packages: dependency: transitive description: name: file_selector_linux - sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" url: "https://pub.dev" source: hosted - version: "0.9.3+2" + version: "0.9.4" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" url: "https://pub.dev" source: hosted - version: "0.9.4+2" + version: "0.9.5" file_selector_platform_interface: dependency: transitive description: name: file_selector_platform_interface - sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" url: "https://pub.dev" source: hosted - version: "2.6.2" + version: "2.7.0" file_selector_windows: dependency: transitive description: name: file_selector_windows - sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" url: "https://pub.dev" source: hosted - version: "0.9.3+4" + version: "0.9.3+5" fixnum: dependency: transitive description: @@ -536,14 +471,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_cache_manager: - dependency: "direct main" - description: - name: flutter_cache_manager - sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" - url: "https://pub.dev" - source: hosted - version: "3.4.1" flutter_displaymode: dependency: "direct main" description: @@ -622,10 +549,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3" + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" url: "https://pub.dev" source: hosted - version: "2.0.27" + version: "2.0.34" flutter_riverpod: dependency: transitive description: @@ -686,10 +613,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678 + sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.4" flutter_test: dependency: "direct dev" description: flutter @@ -699,26 +626,26 @@ packages: dependency: "direct main" description: name: flutter_udid - sha256: "166bee5989a58c66b8b62000ea65edccc7c8167bbafdbb08022638db330dd030" + sha256: fc599671cbe8b328e509c961ec121880406ed994dde659cc9ece9c7503cd31c7 url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.1.2" flutter_web_auth_2: dependency: "direct main" description: name: flutter_web_auth_2 - sha256: "561c32d32ed537853de43852c35849cf1d37f3482f41f22b718ab6112f96b333" + sha256: d354998934ddc338e69b999b2abaeb33c6fd09999d3a5f92ead1a6b49b49712e url: "https://pub.dev" source: hosted - version: "5.0.0-alpha.0" + version: "5.0.2" flutter_web_auth_2_platform_interface: dependency: transitive description: name: flutter_web_auth_2_platform_interface - sha256: "45927587ebb2364cd273675ec95f6f67b81725754b416cef2b65cdc63fd3e853" + sha256: ba0fbba55bffb47242025f96852ad1ffba34bc451568f56ef36e613612baffab url: "https://pub.dev" source: hosted - version: "5.0.0-alpha.0" + version: "5.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -728,18 +655,10 @@ packages: dependency: "direct main" description: name: fluttertoast - sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" + sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8" url: "https://pub.dev" source: hosted - version: "8.2.12" - freezed_annotation: - dependency: transitive - description: - name: freezed_annotation - sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b - url: "https://pub.dev" - source: hosted - version: "3.0.0" + version: "8.2.14" frontend_server_client: dependency: transitive description: @@ -773,18 +692,18 @@ packages: dependency: transitive description: name: geolocator_android - sha256: "114072db5d1dce0ec0b36af2697f55c133bc89a2c8dd513e137c0afe59696ed4" + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" url: "https://pub.dev" source: hosted - version: "5.0.1+1" + version: "5.0.2" geolocator_apple: dependency: transitive description: name: geolocator_apple - sha256: c4ecead17985ede9634f21500072edfcb3dba0ef7b97f8d7bc556d2d722b3ba3 + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 url: "https://pub.dev" source: hosted - version: "2.3.9" + version: "2.3.13" geolocator_linux: dependency: transitive description: @@ -797,10 +716,10 @@ packages: dependency: transitive description: name: geolocator_platform_interface - sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012" + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "4.2.6" geolocator_web: dependency: transitive description: @@ -813,10 +732,10 @@ packages: dependency: transitive description: name: geolocator_windows - sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e" + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.2.5" glob: dependency: transitive description: @@ -849,6 +768,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.8.1" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.dev" + source: hosted + version: "1.0.2" hooks_riverpod: dependency: "direct main" description: @@ -861,10 +788,10 @@ packages: dependency: transitive description: name: hotreloader - sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b + sha256: "66871df468fc24eee81f1a0a7cb98acc104716f9b7376d355437b48d633c4ebf" url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.4.0" html: dependency: transitive description: @@ -877,10 +804,10 @@ packages: dependency: "direct main" description: name: http - sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.6.0" http_multi_server: dependency: transitive description: @@ -909,42 +836,42 @@ packages: dependency: transitive description: name: image - sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce url: "https://pub.dev" source: hosted - version: "4.5.4" + version: "4.8.0" image_picker: dependency: "direct main" description: name: image_picker - sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "58a85e6f09fe9c4484d53d18a0bd6271b72c53fce1d05e6f745ae36d8c18efca" + sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622" url: "https://pub.dev" source: hosted - version: "0.8.13+5" + version: "0.8.13+16" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: e675c22790bcc24e9abd455deead2b7a88de4b79f7327a281812f14de1a56f58 + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 url: "https://pub.dev" source: hosted - version: "0.8.13+1" + version: "0.8.13+6" image_picker_linux: dependency: transitive description: @@ -965,10 +892,10 @@ packages: dependency: transitive description: name: image_picker_platform_interface - sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.11.1" image_picker_windows: dependency: transitive description: @@ -977,13 +904,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" - immich_mobile_immich_lint: - dependency: "direct dev" - description: - path: immich_lint - relative: true - source: path - version: "0.0.0" immich_ui: dependency: "direct main" description: @@ -1012,40 +932,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - isar: - dependency: "direct main" - description: - path: "packages/isar" - ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - resolved-ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - url: "https://github.com/immich-app/isar" - source: git - version: "3.1.8" - isar_community: - dependency: transitive - description: - name: isar_community - sha256: "28f59e54636c45ba0bb1b3b7f2656f1c50325f740cea6efcd101900be3fba546" - url: "https://pub.dev" - source: hosted - version: "3.3.0-dev.3" - isar_community_flutter_libs: - dependency: "direct main" - description: - name: isar_community_flutter_libs - sha256: c2934fe755bb3181cb67602fd5df0d080b3d3eb52799f98623aa4fc5acbea010 - url: "https://pub.dev" - source: hosted - version: "3.3.0-dev.3" - isar_generator: - dependency: "direct dev" - description: - path: "packages/isar_generator" - ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - resolved-ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - url: "https://github.com/immich-app/isar" - source: git - version: "3.1.8" jni: dependency: transitive description: @@ -1066,10 +952,10 @@ packages: dependency: transitive description: name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.11.0" leak_tracker: dependency: transitive description: @@ -1094,6 +980,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + lean_builder: + dependency: transitive + description: + name: lean_builder + sha256: ee4117b03e93a4eb83e1a78c8e7a1dc22188d43bb142309982be48673a1b3a53 + url: "https://pub.dev" + source: hosted + version: "0.1.7" lints: dependency: transitive description: @@ -1114,26 +1008,26 @@ packages: dependency: transitive description: name: local_auth_android - sha256: "63ad7ca6396290626dc0cb34725a939e4cfe965d80d36112f08d49cf13a8136e" + sha256: a0bdfcc0607050a26ef5b31d6b4b254581c3d3ce3c1816ab4d4f4a9173e84467 url: "https://pub.dev" source: hosted - version: "1.0.49" + version: "1.0.56" local_auth_darwin: dependency: transitive description: name: local_auth_darwin - sha256: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2" + sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49" url: "https://pub.dev" source: hosted - version: "1.4.3" + version: "1.6.1" local_auth_platform_interface: dependency: transitive description: name: local_auth_platform_interface - sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a" + sha256: f98b8e388588583d3f781f6806e4f4c9f9e189d898d27f0c249b93a1973dd122 url: "https://pub.dev" source: hosted - version: "1.0.10" + version: "1.1.0" local_auth_windows: dependency: transitive description: @@ -1178,18 +1072,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -1210,10 +1104,18 @@ packages: dependency: "direct dev" description: name: mocktail - sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + sha256: "5e1bf53cc7baa8062a33b84424deb61513858ea05c601b8509e683815b5914aa" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" native_video_player: dependency: "direct main" description: @@ -1227,10 +1129,10 @@ packages: dependency: "direct main" description: name: network_info_plus - sha256: "08f4166bbb77da9e407edef6322a33f87b18c0ca46483fb25606cb3d2bfcdd2a" + sha256: f926b2ba86aa0086a0dfbb9e5072089bc213d854135c1712f1d29fc89ba3c877 url: "https://pub.dev" source: hosted - version: "6.1.3" + version: "6.1.4" network_info_plus_platform_interface: dependency: transitive description: @@ -1251,10 +1153,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "1f81ed9e41909d44162d7ec8663b2c647c202317cc0b56d3d56f6a13146a0b64" + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "9.3.0" octo_image: dependency: "direct main" description: @@ -1283,26 +1185,26 @@ packages: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" url: "https://pub.dev" source: hosted - version: "8.3.0" + version: "8.3.1" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" path: dependency: "direct main" description: @@ -1331,18 +1233,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.23" path_provider_foundation: dependency: "direct main" description: name: path_provider_foundation - sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738 + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -1387,10 +1289,10 @@ packages: dependency: transitive description: name: permission_handler_apple - sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98 + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 url: "https://pub.dev" source: hosted - version: "9.4.6" + version: "9.4.7" permission_handler_html: dependency: transitive description: @@ -1419,26 +1321,26 @@ packages: dependency: transitive description: name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" photo_manager: dependency: "direct main" description: name: photo_manager - sha256: a0d9a7a9bc35eda02d33766412bde6d883a8b0acb86bbe37dac5f691a0894e8a + sha256: fb3bc8ea653370f88742b3baa304700107c83d12748aa58b2b9f2ed3ef15e6c2 url: "https://pub.dev" source: hosted - version: "3.7.1" + version: "3.9.0" pigeon: dependency: "direct dev" description: name: pigeon - sha256: "0045b172d1da43c40cb3f58e80e04b50a65cba20b8b70dc880af04181f7758da" + sha256: "04cfefc8add8b47ddf9ccac8b92bb4edeb67c87f185c623ba0db118ac99334ad" url: "https://pub.dev" source: hosted - version: "26.0.2" + version: "26.3.4" pinput: dependency: "direct main" description: @@ -1467,26 +1369,26 @@ packages: dependency: transitive description: name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.5.2" posix: dependency: transitive description: name: posix - sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.5.0" process: dependency: transitive description: name: process - sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 url: "https://pub.dev" source: hosted - version: "5.0.3" + version: "5.0.5" protobuf: dependency: transitive description: @@ -1535,46 +1437,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.1" - riverpod_analyzer_utils: - dependency: transitive - description: - name: riverpod_analyzer_utils - sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611" - url: "https://pub.dev" - source: hosted - version: "0.5.10" - riverpod_annotation: - dependency: "direct main" - description: - name: riverpod_annotation - sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 - url: "https://pub.dev" - source: hosted - version: "2.6.1" - riverpod_generator: - dependency: "direct dev" - description: - name: riverpod_generator - sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36" - url: "https://pub.dev" - source: hosted - version: "2.6.5" - riverpod_lint: - dependency: "direct dev" - description: - name: riverpod_lint - sha256: "89a52b7334210dbff8605c3edf26cfe69b15062beed5cbfeff2c3812c33c9e35" - url: "https://pub.dev" - source: hosted - version: "2.6.5" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" scroll_date_picker: dependency: "direct main" description: @@ -1643,26 +1505,26 @@ packages: dependency: transitive description: name: shared_preferences - sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.5" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad" + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.23" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.6" shared_preferences_linux: dependency: transitive description: @@ -1675,10 +1537,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" shared_preferences_web: dependency: transitive description: @@ -1745,90 +1607,50 @@ packages: dependency: transitive description: name: source_gen - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "4.2.2" source_span: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "1.10.1" - sprintf: + version: "1.10.2" + sqlcipher_flutter_libs: dependency: transitive description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + name: sqlcipher_flutter_libs + sha256: "38d62d659d2fb8739bf25a42c9a350d1fdd6c29a5a61f13a946778ec75d27929" url: "https://pub.dev" source: hosted - version: "7.0.0" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" - url: "https://pub.dev" - source: hosted - version: "2.5.5" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" - source: hosted - version: "2.4.0" + version: "0.7.0+eol" sqlite3: dependency: transitive description: name: sqlite3 - sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" + sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5" url: "https://pub.dev" source: hosted - version: "2.7.5" + version: "3.3.1" sqlite3_flutter_libs: dependency: transitive description: name: sqlite3_flutter_libs - sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3" + sha256: "3ed7553eee7bb368f8950f58ba29f634e06e813c029aff6a0d60862b96de8454" url: "https://pub.dev" source: hosted - version: "0.5.31" + version: "0.6.0+eol" sqlparser: dependency: transitive description: name: sqlparser - sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee" + sha256: ab2b467425f1d4f3acfa5fd11a08226f7d6c26ff102c06be1807e1dff34e050b url: "https://pub.dev" source: hosted - version: "0.41.0" + version: "0.44.3" stack_trace: dependency: transitive description: @@ -1877,14 +1699,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" - url: "https://pub.dev" - source: hosted - version: "3.3.1" term_glyph: dependency: transitive description: @@ -1897,10 +1711,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.10" thumbhash: dependency: "direct main" description: @@ -1909,14 +1723,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.0+1" - time: - dependency: transitive - description: - name: time - sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" - url: "https://pub.dev" - source: hosted - version: "2.1.5" timezone: dependency: "direct main" description: @@ -1925,14 +1731,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.4" - timing: - dependency: transitive - description: - name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" - url: "https://pub.dev" - source: hosted - version: "1.0.2" typed_data: dependency: transitive description: @@ -1945,10 +1743,10 @@ packages: dependency: transitive description: name: universal_io - sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.1" universal_platform: dependency: transitive description: @@ -1969,34 +1767,34 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" + sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" url: "https://pub.dev" source: hosted - version: "6.3.15" + version: "6.3.29" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.4.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.5" url_launcher_platform_interface: dependency: transitive description: @@ -2009,34 +1807,34 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.2" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" uuid: dependency: "direct main" description: name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" url: "https://pub.dev" source: hosted - version: "4.5.1" + version: "4.5.3" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + sha256: "81da85e9ca8885ade47f9685b953cb098970d11be4821ac765580a6607ea4373" url: "https://pub.dev" source: hosted - version: "1.1.18" + version: "1.1.21" vector_graphics_codec: dependency: transitive description: @@ -2049,10 +1847,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" url: "https://pub.dev" source: hosted - version: "1.1.19" + version: "1.2.0" vector_math: dependency: transitive description: @@ -2065,10 +1863,10 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "15.1.0" wakelock_plus: dependency: "direct main" description: @@ -2081,18 +1879,18 @@ packages: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" + sha256: "14b2e5b9e35c2631e656913c47adecdd71633ae92896a27a64c8f1fcfabc21cc" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" watcher: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.1" web: dependency: transitive description: @@ -2129,10 +1927,10 @@ packages: dependency: transitive description: name: win32 - sha256: b89e6e24d1454e149ab20fbb225af58660f0c0bf4475544650700d8e2da54aef + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e url: "https://pub.dev" source: hosted - version: "5.11.0" + version: "5.15.0" win32_registry: dependency: transitive description: @@ -2153,10 +1951,10 @@ packages: dependency: "direct main" description: name: worker_manager - sha256: "1bce9f894a0c187856f5fc0e150e7fe1facce326f048ca6172947754dac3d4f3" + sha256: "887587eb97e517bca88dea761bea96edc495513ec91e4c489dcf110967ba79ff" url: "https://pub.dev" source: hosted - version: "7.2.7" + version: "7.2.9" xdg_directories: dependency: transitive description: @@ -2190,5 +1988,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.7" + dart: ">=3.11.0 <4.0.0" + flutter: "3.41.7" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 0c9ca46250..351d6869b3 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,74 +2,64 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 2.6.0+3038 +version: 2.7.5+3046 environment: - sdk: '>=3.8.0 <4.0.0' - flutter: 3.35.7 + sdk: '>=3.11.0 <4.0.0' + flutter: 3.41.7 dependencies: - async: ^2.13.0 - auto_route: ^9.2.0 - background_downloader: ^9.3.0 + async: ^2.13.1 + auto_route: ^11.1.0 + background_downloader: ^9.5.4 cast: ^2.1.0 collection: ^1.19.1 - connectivity_plus: ^6.1.3 - crop_image: ^1.0.16 - crypto: ^3.0.6 - device_info_plus: ^12.2.0 - # DB - drift: ^2.26.0 - drift_flutter: ^0.2.6 + connectivity_plus: ^6.1.5 + crop_image: ^1.0.17 + crypto: ^3.0.7 + device_info_plus: ^12.4.0 + drift: ^2.32.1 + drift_flutter: ^0.3.0 dynamic_color: ^1.8.1 easy_localization: ^3.0.8 - ffi: ^2.1.4 + ffi: ^2.2.0 flutter: sdk: flutter - flutter_cache_manager: ^3.4.1 flutter_displaymode: ^0.7.0 flutter_hooks: ^0.21.3+1 - flutter_local_notifications: ^17.2.1+2 + flutter_local_notifications: ^17.2.4 flutter_secure_storage: ^9.2.4 - flutter_svg: ^2.2.1 - flutter_udid: ^4.0.0 - flutter_web_auth_2: ^5.0.0-alpha.0 - fluttertoast: ^8.2.12 + flutter_svg: ^2.2.4 + flutter_udid: ^4.1.2 + flutter_web_auth_2: ^5.0.2 + fluttertoast: ^8.2.14 geolocator: ^14.0.2 home_widget: ^0.8.1 hooks_riverpod: ^2.6.1 - http: ^1.5.0 - image_picker: ^1.2.0 + http: ^1.6.0 + image_picker: ^1.2.1 immich_ui: path: './packages/ui' intl: ^0.20.2 - isar: - git: - url: https://github.com/immich-app/isar - ref: 'bb1dca40fe87a001122e5d43bc6254718cb49f3a' - path: packages/isar/ - isar_community_flutter_libs: 3.3.0-dev.3 local_auth: ^2.3.0 logging: ^1.3.0 maplibre_gl: ^0.22.0 - native_video_player: git: url: https://github.com/immich-app/native_video_player ref: 'cdf621bdb7edaf996e118a58a48f6441187d79c6' - network_info_plus: ^6.1.3 + network_info_plus: ^6.1.4 octo_image: ^2.1.0 openapi: path: openapi - package_info_plus: ^8.3.0 + package_info_plus: ^8.3.1 path: ^1.9.1 path_provider: ^2.1.5 - path_provider_foundation: ^2.4.3 + path_provider_foundation: ^2.6.0 permission_handler: ^11.4.0 - photo_manager: ^3.7.1 + photo_manager: ^3.9.0 pinput: ^5.0.2 punycode: ^1.0.0 - riverpod_annotation: ^2.6.1 scroll_date_picker: ^3.8.0 scrollable_positioned_list: ^0.3.8 share_handler: ^0.0.25 @@ -79,9 +69,9 @@ dependencies: thumbhash: 0.1.0+1 timezone: ^0.9.4 url_launcher: ^6.3.2 - uuid: ^4.5.1 - wakelock_plus: ^1.3.0 - worker_manager: ^7.2.7 + uuid: ^4.5.3 + wakelock_plus: ^1.3.3 + worker_manager: ^7.2.9 web_socket: ^1.0.1 socket_io_client: git: @@ -99,11 +89,10 @@ dependencies: path: pkgs/ok_http/ dev_dependencies: - auto_route_generator: ^9.0.0 - build_runner: ^2.4.8 - custom_lint: ^0.7.5 + auto_route_generator: ^10.5.0 + build_runner: ^2.13.1 # Drift generator - drift_dev: ^2.26.0 + drift_dev: ^2.32.1 fake_async: ^1.3.3 file: ^7.0.1 # for MemoryFileSystem flutter_launcher_icons: ^0.14.4 @@ -111,27 +100,16 @@ dev_dependencies: flutter_native_splash: ^2.4.7 flutter_test: sdk: flutter - immich_mobile_immich_lint: - path: './immich_lint' integration_test: sdk: flutter - isar_generator: - git: - url: https://github.com/immich-app/isar - ref: 'bb1dca40fe87a001122e5d43bc6254718cb49f3a' - path: packages/isar_generator/ - mocktail: ^1.0.4 + mocktail: ^1.0.5 # Type safe platform code - pigeon: ^26.0.2 - riverpod_generator: ^2.6.1 - riverpod_lint: ^2.6.1 + pigeon: ^26.3.4 +# cast 2.1.0 declares a loose bonsoir range but its code targets the 5.x API. +# Pin bonsoir to 5.x until cast releases a version compatible with bonsoir 6.x. dependency_overrides: - drift: - git: - url: https://github.com/immich-app/drift - ref: '53ef7e9f19fe8f68416251760b4b99fe43f1c575' - path: drift/ + bonsoir: ^5.1.11 flutter: uses-material-design: true diff --git a/mobile/scripts/fdroid_build_isar.sh b/mobile/scripts/fdroid_build_isar.sh deleted file mode 100755 index a145268356..0000000000 --- a/mobile/scripts/fdroid_build_isar.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env sh - -test -d .isar || exit -cp .isar-cargo.lock .isar/Cargo.lock -(cd .isar || exit -bash tool/build_android.sh x86 -bash tool/build_android.sh x64 -bash tool/build_android.sh armv7 -bash tool/build_android.sh arm64 -mv libisar_android_arm64.so libisar.so -mv libisar.so ../.pub-cache/hosted/pub.dev/isar_community_flutter_libs-*/android/src/main/jniLibs/arm64-v8a/ -mv libisar_android_armv7.so libisar.so -mv libisar.so ../.pub-cache/hosted/pub.dev/isar_community_flutter_libs-*/android/src/main/jniLibs/armeabi-v7a/ -mv libisar_android_x64.so libisar.so -mv libisar.so ../.pub-cache/hosted/pub.dev/isar_community_flutter_libs-*/android/src/main/jniLibs/x86_64/ -mv libisar_android_x86.so libisar.so -mv libisar.so ../.pub-cache/hosted/pub.dev/isar_community_flutter_libs-*/android/src/main/jniLibs/x86/ -) diff --git a/mobile/scripts/fdroid_update_isar.sh b/mobile/scripts/fdroid_update_isar.sh deleted file mode 100755 index 814f50a8a1..0000000000 --- a/mobile/scripts/fdroid_update_isar.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env sh - -isar_version="$(awk '/isar: /{gsub(/\^/, "", $2); print $2}' pubspec.yaml)" -checked_out_version="$(git -C .isar describe --tags)" - -if [ "$isar_version" = "$checked_out_version" ]; then - echo "isar is up-to-date." - exit 0 -fi -echo "Updating from version $checked_out_version to $isar_version." - -git -C .isar checkout "$isar_version" -cargo generate-lockfile --manifest-path .isar/Cargo.toml -mv .isar/Cargo.lock .isar-cargo.lock diff --git a/mobile/test/api.mocks.dart b/mobile/test/api.mocks.dart index c6a3a90582..e1c32eaaee 100644 --- a/mobile/test/api.mocks.dart +++ b/mobile/test/api.mocks.dart @@ -1,8 +1,6 @@ import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; -class MockAssetsApi extends Mock implements AssetsApi {} - class MockSyncApi extends Mock implements SyncApi {} class MockServerApi extends Mock implements ServerApi {} diff --git a/mobile/test/domain/service.mock.dart b/mobile/test/domain/service.mock.dart index 56b4802f88..89e85a3794 100644 --- a/mobile/test/domain/service.mock.dart +++ b/mobile/test/domain/service.mock.dart @@ -1,20 +1,13 @@ import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background_upload.service.dart'; import 'package:mocktail/mocktail.dart'; class MockStoreService extends Mock implements StoreService {} -class MockUserService extends Mock implements UserService {} - class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {} class MockNativeSyncApi extends Mock implements NativeSyncApi {} class MockAppSettingsService extends Mock implements AppSettingsService {} - -class MockBackgroundUploadService extends Mock implements BackgroundUploadService {} - diff --git a/mobile/test/domain/services/album.service_test.dart b/mobile/test/domain/services/album.service_test.dart deleted file mode 100644 index 9110a09471..0000000000 --- a/mobile/test/domain/services/album.service_test.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/domain/services/remote_album.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; - -void main() { - late RemoteAlbumService sut; - late DriftRemoteAlbumRepository mockRemoteAlbumRepo; - late DriftAlbumApiRepository mockAlbumApiRepo; - - final albumA = RemoteAlbum( - id: '1', - name: 'Album A', - description: "", - isActivityEnabled: false, - order: AlbumAssetOrder.asc, - assetCount: 1, - createdAt: DateTime(2023, 1, 1), - updatedAt: DateTime(2023, 1, 2), - ownerId: 'owner1', - ownerName: "Test User", - isShared: false, - ); - - final albumB = RemoteAlbum( - id: '2', - name: 'Album B', - description: "", - isActivityEnabled: false, - order: AlbumAssetOrder.desc, - assetCount: 2, - createdAt: DateTime(2023, 2, 1), - updatedAt: DateTime(2023, 2, 2), - ownerId: 'owner2', - ownerName: "Test User", - isShared: false, - ); - - setUp(() { - mockRemoteAlbumRepo = MockRemoteAlbumRepository(); - mockAlbumApiRepo = MockDriftAlbumApiRepository(); - - when( - () => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.end), - ).thenAnswer((_) async => ['1', '2']); - - when( - () => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.start), - ).thenAnswer((_) async => ['1', '2']); - - sut = RemoteAlbumService(mockRemoteAlbumRepo, mockAlbumApiRepo); - }); - - group('sortAlbums', () { - test('should sort correctly based on name', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.title); - expect(result, [albumA, albumB]); - }); - - test('should sort correctly based on createdAt', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.created); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on updatedAt', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.lastModified); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on assetCount', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.assetCount); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on newestAssetTimestamp', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.mostRecent); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on oldestAssetTimestamp', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.mostOldest); - expect(result, [albumA, albumB]); - }); - - test('should flip order when isReverse is true for all modes', () async { - final albums = [albumB, albumA]; - - for (final mode in AlbumSortMode.values) { - final normal = await sut.sortAlbums(albums, mode, isReverse: false); - final reversed = await sut.sortAlbums(albums, mode, isReverse: true); - - // reversed should be the exact inverse of normal - expect(reversed, normal.reversed.toList(), reason: 'Mode: $mode'); - } - }); - }); -} diff --git a/mobile/test/domain/services/asset.service_test.dart b/mobile/test/domain/services/asset.service_test.dart deleted file mode 100644 index 04e49f89f9..0000000000 --- a/mobile/test/domain/services/asset.service_test.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/domain/services/asset.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; -import '../../test_utils.dart'; - -void main() { - late AssetService sut; - late MockRemoteAssetRepository mockRemoteAssetRepository; - late MockDriftLocalAssetRepository mockLocalAssetRepository; - - setUp(() { - mockRemoteAssetRepository = MockRemoteAssetRepository(); - mockLocalAssetRepository = MockDriftLocalAssetRepository(); - sut = AssetService( - remoteAssetRepository: mockRemoteAssetRepository, - localAssetRepository: mockLocalAssetRepository, - ); - }); - - group('getAspectRatio', () { - test('flips dimensions on Android for 90° and 270° orientations', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - for (final orientation in [90, 270]) { - final localAsset = TestUtils.createLocalAsset( - id: 'local-$orientation', - width: 1920, - height: 1080, - orientation: orientation, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip on Android'); - } - }); - - test('does not flip dimensions on iOS regardless of orientation', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - for (final orientation in [0, 90, 270]) { - final localAsset = TestUtils.createLocalAsset( - id: 'local-$orientation', - width: 1920, - height: 1080, - orientation: orientation, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1920 / 1080, reason: 'iOS should never flip dimensions'); - } - }); - - test('fetches dimensions from remote repository when missing from asset', () async { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null); - - final exif = const ExifInfo(orientation: '1'); - - final fetchedAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 1080); - - when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); - when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => fetchedAsset); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1920 / 1080); - verify(() => mockRemoteAssetRepository.get('remote-1')).called(1); - }); - - test('fetches dimensions from local repository when missing from local asset', () async { - final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0); - - final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 0); - - when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1920 / 1080); - verify(() => mockLocalAssetRepository.get('local-1')).called(1); - }); - - test('uses fetched asset orientation when dimensions are missing on Android', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - // Original asset has default orientation 0, but dimensions are missing - final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0); - - // Fetched asset has 90° orientation and proper dimensions - final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 90); - - when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset); - - final result = await sut.getAspectRatio(localAsset); - - // Should flip dimensions since fetched asset has 90° orientation - expect(result, 1080 / 1920); - verify(() => mockLocalAssetRepository.get('local-1')).called(1); - }); - - test('returns 1.0 when dimensions are still unavailable after fetching', () async { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null); - - final exif = const ExifInfo(orientation: '1'); - - when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); - when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => null); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1.0); - }); - - test('returns 1.0 when height is zero', () async { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 0); - - final exif = const ExifInfo(orientation: '1'); - - when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1.0); - }); - - test('handles local asset with remoteId using local orientation not remote exif', () async { - // When a LocalAsset has a remoteId (merged), we should use local orientation - // because the width/height come from the local asset (pre-corrected on iOS) - final localAsset = TestUtils.createLocalAsset( - id: 'local-1', - remoteId: 'remote-1', - width: 1920, - height: 1080, - orientation: 0, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1920 / 1080); - // Should not call remote exif for LocalAsset - verifyNever(() => mockRemoteAssetRepository.getExif(any())); - }); - - test('handles local asset with remoteId and 90 degree rotation on Android', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - final localAsset = TestUtils.createLocalAsset( - id: 'local-1', - remoteId: 'remote-1', - width: 1920, - height: 1080, - orientation: 90, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1080 / 1920); - }); - - test('should not flip remote asset dimensions', () async { - final flippedOrientations = ['1', '2', '3', '4', '5', '6', '7', '8', '90', '-90']; - - for (final orientation in flippedOrientations) { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080); - - final exif = ExifInfo(orientation: orientation); - - when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1920 / 1080, reason: 'Should not flipped remote asset dimensions for orientation $orientation'); - } - }); - }); -} diff --git a/mobile/test/domain/services/log_service_test.dart b/mobile/test/domain/services/log_service_test.dart index 95f677ba98..0ccef393ab 100644 --- a/mobile/test/domain/services/log_service_test.dart +++ b/mobile/test/domain/services/log_service_test.dart @@ -29,11 +29,11 @@ final _kWarnLog = LogMessage( void main() { late LogService sut; late LogRepository mockLogRepo; - late IsarStoreRepository mockStoreRepo; + late DriftStoreRepository mockStoreRepo; setUp(() async { mockLogRepo = MockLogRepository(); - mockStoreRepo = MockStoreRepository(); + mockStoreRepo = MockDriftStoreRepository(); registerFallbackValue(_kInfoLog); diff --git a/mobile/test/domain/services/store_service_test.dart b/mobile/test/domain/services/store_service_test.dart index 996170b518..8ceb1e3c9c 100644 --- a/mobile/test/domain/services/store_service_test.dart +++ b/mobile/test/domain/services/store_service_test.dart @@ -15,13 +15,11 @@ final _kBackupFailedSince = DateTime.utc(2023); void main() { late StoreService sut; - late IsarStoreRepository mockStoreRepo; late DriftStoreRepository mockDriftStoreRepo; late StreamController>> controller; setUp(() async { controller = StreamController>>.broadcast(); - mockStoreRepo = MockStoreRepository(); mockDriftStoreRepo = MockDriftStoreRepository(); // For generics, we need to provide fallback to each concrete type to avoid runtime errors registerFallbackValue(StoreKey.accessToken); @@ -29,16 +27,6 @@ void main() { registerFallbackValue(StoreKey.backgroundBackup); registerFallbackValue(StoreKey.backupFailedSince); - when(() => mockStoreRepo.getAll()).thenAnswer( - (_) async => [ - const StoreDto(StoreKey.accessToken, _kAccessToken), - const StoreDto(StoreKey.backgroundBackup, _kBackgroundBackup), - const StoreDto(StoreKey.groupAssetsBy, _kGroupAssetsBy), - StoreDto(StoreKey.backupFailedSince, _kBackupFailedSince), - ], - ); - when(() => mockStoreRepo.watchAll()).thenAnswer((_) => controller.stream); - when(() => mockDriftStoreRepo.getAll()).thenAnswer( (_) async => [ const StoreDto(StoreKey.accessToken, _kAccessToken), @@ -49,7 +37,7 @@ void main() { ); when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream); - sut = await StoreService.create(storeRepository: mockStoreRepo); + sut = await StoreService.create(storeRepository: mockDriftStoreRepo); }); tearDown(() async { @@ -59,7 +47,7 @@ void main() { group("Store Service Init:", () { test('Populates the internal cache on init', () { - verify(() => mockStoreRepo.getAll()).called(1); + verify(() => mockDriftStoreRepo.getAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), _kAccessToken); expect(sut.tryGet(StoreKey.backgroundBackup), _kBackgroundBackup); expect(sut.tryGet(StoreKey.groupAssetsBy), _kGroupAssetsBy); @@ -74,7 +62,7 @@ void main() { await pumpEventQueue(); - verify(() => mockStoreRepo.watchAll()).called(1); + verify(() => mockDriftStoreRepo.watchAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), _kAccessToken.toUpperCase()); }); }); @@ -95,19 +83,18 @@ void main() { group('Store Service put:', () { setUp(() { - when(() => mockStoreRepo.upsert(any>(), any())).thenAnswer((_) async => true); when(() => mockDriftStoreRepo.upsert(any>(), any())).thenAnswer((_) async => true); }); test('Skip insert when value is not modified', () async { await sut.put(StoreKey.accessToken, _kAccessToken); - verifyNever(() => mockStoreRepo.upsert(StoreKey.accessToken, any())); + verifyNever(() => mockDriftStoreRepo.upsert(StoreKey.accessToken, any())); }); test('Insert value when modified', () async { final newAccessToken = _kAccessToken.toUpperCase(); await sut.put(StoreKey.accessToken, newAccessToken); - verify(() => mockStoreRepo.upsert(StoreKey.accessToken, newAccessToken)).called(1); + verify(() => mockDriftStoreRepo.upsert(StoreKey.accessToken, newAccessToken)).called(1); expect(sut.tryGet(StoreKey.accessToken), newAccessToken); }); }); @@ -117,7 +104,6 @@ void main() { setUp(() { valueController = StreamController.broadcast(); - when(() => mockStoreRepo.watch(any>())).thenAnswer((_) => valueController.stream); when(() => mockDriftStoreRepo.watch(any>())).thenAnswer((_) => valueController.stream); }); @@ -136,19 +122,18 @@ void main() { } await pumpEventQueue(); - verify(() => mockStoreRepo.watch(StoreKey.accessToken)).called(1); + verify(() => mockDriftStoreRepo.watch(StoreKey.accessToken)).called(1); }); }); group('Store Service delete:', () { setUp(() { - when(() => mockStoreRepo.delete(any>())).thenAnswer((_) async => true); when(() => mockDriftStoreRepo.delete(any>())).thenAnswer((_) async => true); }); test('Removes the value from the DB', () async { await sut.delete(StoreKey.accessToken); - verify(() => mockStoreRepo.delete(StoreKey.accessToken)).called(1); + verify(() => mockDriftStoreRepo.delete(StoreKey.accessToken)).called(1); }); test('Removes the value from the cache', () async { @@ -159,13 +144,12 @@ void main() { group('Store Service clear:', () { setUp(() { - when(() => mockStoreRepo.deleteAll()).thenAnswer((_) async => true); when(() => mockDriftStoreRepo.deleteAll()).thenAnswer((_) async => true); }); test('Clears all values from the store', () async { await sut.clear(); - verify(() => mockStoreRepo.deleteAll()).called(1); + verify(() => mockDriftStoreRepo.deleteAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), isNull); expect(sut.tryGet(StoreKey.backgroundBackup), isNull); expect(sut.tryGet(StoreKey.groupAssetsBy), isNull); diff --git a/mobile/test/domain/services/user_service_test.dart b/mobile/test/domain/services/user_service_test.dart index 395f38a207..80b6d80457 100644 --- a/mobile/test/domain/services/user_service_test.dart +++ b/mobile/test/domain/services/user_service_test.dart @@ -4,7 +4,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -14,19 +13,13 @@ import '../service.mock.dart'; void main() { late UserService sut; - late IsarUserRepository mockUserRepo; late UserApiRepository mockUserApiRepo; late StoreService mockStoreService; setUp(() { - mockUserRepo = MockIsarUserRepository(); mockUserApiRepo = MockUserApiRepository(); mockStoreService = MockStoreService(); - sut = UserService( - isarUserRepository: mockUserRepo, - userApiRepository: mockUserApiRepo, - storeService: mockStoreService, - ); + sut = UserService(userApiRepository: mockUserApiRepo, storeService: mockStoreService); registerFallbackValue(UserStub.admin); when(() => mockStoreService.get(StoreKey.currentUser)).thenReturn(UserStub.admin); @@ -77,11 +70,9 @@ void main() { test('should return user from api and store it', () async { when(() => mockUserApiRepo.getMyUser()).thenAnswer((_) async => UserStub.admin); when(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).thenAnswer((_) async => true); - when(() => mockUserRepo.update(UserStub.admin)).thenAnswer((_) async => UserStub.admin); final result = await sut.refreshMyUser(); verify(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).called(1); - verify(() => mockUserRepo.update(UserStub.admin)).called(1); expect(result, UserStub.admin); }); @@ -90,7 +81,6 @@ void main() { final result = await sut.refreshMyUser(); verifyNever(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)); - verifyNever(() => mockUserRepo.update(UserStub.admin)); expect(result, isNull); }); }); @@ -104,12 +94,10 @@ void main() { () => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)), ).thenAnswer((_) async => profileImagePath); when(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).thenAnswer((_) async => true); - when(() => mockUserRepo.update(updatedUser)).thenAnswer((_) async => UserStub.admin); final result = await sut.createProfileImage(profileImagePath, Uint8List(0)); verify(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).called(1); - verify(() => mockUserRepo.update(updatedUser)).called(1); expect(result, profileImagePath); }); @@ -123,7 +111,6 @@ void main() { final result = await sut.createProfileImage(profileImagePath, Uint8List(0)); verifyNever(() => mockStoreService.put(StoreKey.currentUser, updatedUser)); - verifyNever(() => mockUserRepo.update(updatedUser)); expect(result, isNull); }); }); diff --git a/mobile/test/dto.mocks.dart b/mobile/test/dto.mocks.dart deleted file mode 100644 index ed53fcdc90..0000000000 --- a/mobile/test/dto.mocks.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; - -class MockSmartSearchDto extends Mock implements SmartSearchDto {} - -class MockMetadataSearchDto extends Mock implements MetadataSearchDto {} diff --git a/mobile/test/fixtures/album.stub.dart b/mobile/test/fixtures/album.stub.dart index a22a4b72ab..5141540a25 100644 --- a/mobile/test/fixtures/album.stub.dart +++ b/mobile/test/fixtures/album.stub.dart @@ -1,108 +1,4 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; - -import 'asset.stub.dart'; -import 'user.stub.dart'; - -final class AlbumStub { - const AlbumStub._(); - - static final emptyAlbum = Album( - name: "empty-album", - localId: "empty-album-local", - remoteId: "empty-album-remote", - createdAt: DateTime(2000), - modifiedAt: DateTime(2023), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - ); - - static final sharedWithUser = Album( - name: "empty-album-shared-with-user", - localId: "empty-album-shared-with-user-local", - remoteId: "empty-album-shared-with-user-remote", - createdAt: DateTime(2023), - modifiedAt: DateTime(2023), - shared: true, - activityEnabled: false, - endDate: DateTime(2020), - )..sharedUsers.addAll([User.fromDto(UserStub.admin)]); - - static final oneAsset = Album( - name: "album-with-single-asset", - localId: "album-with-single-asset-local", - remoteId: "album-with-single-asset-remote", - createdAt: DateTime(2022), - modifiedAt: DateTime(2023), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2023), - )..assets.addAll([AssetStub.image1]); - - static final twoAsset = - Album( - name: "album-with-two-assets", - localId: "album-with-two-assets-local", - remoteId: "album-with-two-assets-remote", - createdAt: DateTime(2001), - modifiedAt: DateTime(2010), - shared: false, - activityEnabled: false, - startDate: DateTime(2019), - endDate: DateTime(2020), - ) - ..assets.addAll([AssetStub.image1, AssetStub.image2]) - ..activityEnabled = true - ..owner.value = User.fromDto(UserStub.admin); - - static final create2020end2020Album = Album( - name: "create2020update2020Album", - localId: "create2020update2020Album-local", - remoteId: "create2020update2020Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2020), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2020), - ); - static final create2020end2022Album = Album( - name: "create2020update2021Album", - localId: "create2020update2021Album-local", - remoteId: "create2020update2021Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2022), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2022), - ); - static final create2020end2024Album = Album( - name: "create2020update2022Album", - localId: "create2020update2022Album-local", - remoteId: "create2020update2022Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2024), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2024), - ); - static final create2020end2026Album = Album( - name: "create2020update2023Album", - localId: "create2020update2023Album-local", - remoteId: "create2020update2023Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2026), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2026), - ); -} abstract final class LocalAlbumStub { const LocalAlbumStub._(); diff --git a/mobile/test/fixtures/asset.stub.dart b/mobile/test/fixtures/asset.stub.dart index 90a7f11737..473b900271 100644 --- a/mobile/test/fixtures/asset.stub.dart +++ b/mobile/test/fixtures/asset.stub.dart @@ -1,59 +1,4 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as old; - -final class AssetStub { - const AssetStub._(); - - static final image1 = old.Asset( - checksum: "image1-checksum", - localId: "image1", - remoteId: 'image1-remote', - ownerId: 1, - fileCreatedAt: DateTime(2019), - fileModifiedAt: DateTime(2020), - updatedAt: DateTime.now(), - durationInSeconds: 0, - type: old.AssetType.image, - fileName: "image1.jpg", - isFavorite: true, - isArchived: false, - isTrashed: false, - exifInfo: const ExifInfo(isFlipped: false), - ); - - static final image2 = old.Asset( - checksum: "image2-checksum", - localId: "image2", - remoteId: 'image2-remote', - ownerId: 1, - fileCreatedAt: DateTime(2000), - fileModifiedAt: DateTime(2010), - updatedAt: DateTime.now(), - durationInSeconds: 60, - type: old.AssetType.video, - fileName: "image2.jpg", - isFavorite: false, - isArchived: false, - isTrashed: false, - exifInfo: const ExifInfo(isFlipped: true), - ); - - static final image3 = old.Asset( - checksum: "image3-checksum", - localId: "image3", - ownerId: 1, - fileCreatedAt: DateTime(2025), - fileModifiedAt: DateTime(2025), - updatedAt: DateTime.now(), - durationInSeconds: 60, - type: old.AssetType.image, - fileName: "image3.jpg", - isFavorite: true, - isArchived: false, - isTrashed: false, - ); -} abstract final class LocalAssetStub { const LocalAssetStub._(); diff --git a/mobile/test/fixtures/exif.stub.dart b/mobile/test/fixtures/exif.stub.dart deleted file mode 100644 index 5ad9a41761..0000000000 --- a/mobile/test/fixtures/exif.stub.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:immich_mobile/domain/models/exif.model.dart'; - -abstract final class ExifStub { - static final size = const ExifInfo(assetId: 1, fileSize: 1000); - - static final gps = const ExifInfo( - assetId: 2, - latitude: 20, - longitude: 20, - city: 'city', - state: 'state', - country: 'country', - ); - - static final rotated90CW = const ExifInfo(assetId: 3, orientation: "90"); - - static final rotated270CW = const ExifInfo(assetId: 4, orientation: "-90"); -} diff --git a/mobile/test/fixtures/user.stub.dart b/mobile/test/fixtures/user.stub.dart index 2ba7177f89..b92ba71e5b 100644 --- a/mobile/test/fixtures/user.stub.dart +++ b/mobile/test/fixtures/user.stub.dart @@ -12,24 +12,4 @@ abstract final class UserStub { profileChangedAt: DateTime(2021), avatarColor: AvatarColor.green, ); - - static final user1 = UserDto( - id: "user1", - email: "user1@test.com", - name: "user1", - isAdmin: false, - updatedAt: DateTime(2022), - profileChangedAt: DateTime(2022), - avatarColor: AvatarColor.red, - ); - - static final user2 = UserDto( - id: "user2", - email: "user2@test.com", - name: "user2", - isAdmin: false, - updatedAt: DateTime(2023), - profileChangedAt: DateTime(2023), - avatarColor: AvatarColor.primary, - ); } diff --git a/mobile/test/infrastructure/repositories/exif_repository_test.dart b/mobile/test/infrastructure/repositories/exif_repository_test.dart deleted file mode 100644 index 4e7ee4d79d..0000000000 --- a/mobile/test/infrastructure/repositories/exif_repository_test.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:isar/isar.dart'; - -import '../../fixtures/exif.stub.dart'; -import '../../test_utils.dart'; - -Future _populateExifTable(Isar db) async { - await db.writeTxn(() async { - await db.exifInfos.putAll([ - ExifInfo.fromDto(ExifStub.size), - ExifInfo.fromDto(ExifStub.gps), - ExifInfo.fromDto(ExifStub.rotated90CW), - ExifInfo.fromDto(ExifStub.rotated270CW), - ]); - }); -} - -void main() { - late Isar db; - late IsarExifRepository sut; - - setUp(() async { - db = await TestUtils.initIsar(); - sut = IsarExifRepository(db); - }); - - group("Return with proper orientation", () { - setUp(() async { - await _populateExifTable(db); - }); - - test("isFlipped true for 90CW", () async { - final exif = await sut.get(ExifStub.rotated90CW.assetId!); - expect(exif!.isFlipped, true); - }); - - test("isFlipped true for 270CW", () async { - final exif = await sut.get(ExifStub.rotated270CW.assetId!); - expect(exif!.isFlipped, true); - }); - - test("isFlipped false for the original non-rotated image", () async { - final exif = await sut.get(ExifStub.size.assetId!); - expect(exif!.isFlipped, false); - }); - }); -} diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart index 18d41e32e0..4cf1adc6b1 100644 --- a/mobile/test/infrastructure/repositories/store_repository_test.dart +++ b/mobile/test/infrastructure/repositories/store_repository_test.dart @@ -1,14 +1,15 @@ import 'dart:async'; +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:isar/isar.dart'; import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; const _kTestAccessToken = "#TestToken"; final _kTestBackupFailed = DateTime(2025, 2, 20, 11, 45); @@ -16,30 +17,54 @@ const _kTestVersion = 10; const _kTestColorfulInterface = false; final _kTestUser = UserStub.admin; -Future _addIntStoreValue(Isar db, StoreKey key, int? value) async { - await db.storeValues.put(StoreValue(key.id, intValue: value, strValue: null)); -} - -Future _addStrStoreValue(Isar db, StoreKey key, String? value) async { - await db.storeValues.put(StoreValue(key.id, intValue: null, strValue: value)); -} - -Future _populateStore(Isar db) async { - await db.writeTxn(() async { - await _addIntStoreValue(db, StoreKey.colorfulInterface, _kTestColorfulInterface ? 1 : 0); - await _addIntStoreValue(db, StoreKey.backupFailedSince, _kTestBackupFailed.millisecondsSinceEpoch); - await _addStrStoreValue(db, StoreKey.accessToken, _kTestAccessToken); - await _addIntStoreValue(db, StoreKey.version, _kTestVersion); +Future _populateStore(Drift db) async { + await db.batch((batch) async { + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.colorfulInterface.id), + intValue: const Value(_kTestColorfulInterface ? 1 : 0), + stringValue: const Value(null), + ), + ); + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.backupFailedSince.id), + intValue: Value(_kTestBackupFailed.millisecondsSinceEpoch), + stringValue: const Value(null), + ), + ); + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.accessToken.id), + intValue: const Value(null), + stringValue: const Value(_kTestAccessToken), + ), + ); + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.version.id), + intValue: const Value(_kTestVersion), + stringValue: const Value(null), + ), + ); }); } void main() { - late Isar db; - late IsarStoreRepository sut; + late Drift db; + late DriftStoreRepository sut; setUp(() async { - db = await TestUtils.initIsar(); - sut = IsarStoreRepository(db); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + sut = DriftStoreRepository(db); + }); + + tearDown(() async { + await db.close(); }); group('Store Repository converters:', () { @@ -98,10 +123,10 @@ void main() { }); test('deleteAll()', () async { - final count = await db.storeValues.count(); + final count = await db.storeEntity.count().getSingle(); expect(count, isNot(isZero)); await sut.deleteAll(); - unawaited(expectLater(await db.storeValues.count(), isZero)); + unawaited(expectLater(await db.storeEntity.count().getSingle(), isZero)); }); }); diff --git a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart index 85eebacb14..d538b567bd 100644 --- a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart +++ b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart @@ -1,10 +1,13 @@ import 'dart:async'; import 'dart:convert'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; 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/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/utils/semver.dart'; @@ -13,7 +16,6 @@ import 'package:openapi/api.dart'; import '../../api.mocks.dart'; import '../../service.mocks.dart'; -import '../../test_utils.dart'; class MockHttpClient extends Mock implements http.Client {} @@ -38,7 +40,8 @@ void main() { late int testBatchSize = 3; setUpAll(() async { - await StoreService.init(storeRepository: IsarStoreRepository(await TestUtils.initIsar())); + final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); }); setUp(() { @@ -137,7 +140,7 @@ void main() { bool abortWasCalledInCallback = false; final Completer firstBatchReceived = Completer(); - Future onDataCallback(List events, Function() abort, Function() _) async { + Future onDataCallback(List _, Function() abort, Function() _) async { onDataCallCount++; if (onDataCallCount == 1) { abort(); @@ -241,7 +244,7 @@ void main() { final streamError = Exception("Network Error"); int onDataCallCount = 0; - Future onDataCallback(List events, Function() _, Function() __) async { + Future onDataCallback(List _, Function() _, Function() __) async { onDataCallCount++; } @@ -267,7 +270,7 @@ void main() { when(() => mockStreamedResponse.stream).thenAnswer((_) => http.ByteStream(errorBodyController.stream)); int onDataCallCount = 0; - Future onDataCallback(List events, Function() _, Function() __) async { + Future onDataCallback(List _, Function() _, Function() __) async { onDataCallCount++; } diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index 2d4af5b308..b7992c1822 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -1,5 +1,4 @@ import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; @@ -11,22 +10,15 @@ import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.da import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:mocktail/mocktail.dart'; -class MockStoreRepository extends Mock implements IsarStoreRepository {} - class MockDriftStoreRepository extends Mock implements DriftStoreRepository {} class MockLogRepository extends Mock implements LogRepository {} -class MockIsarUserRepository extends Mock implements IsarUserRepository {} - -class MockDeviceAssetRepository extends Mock implements IsarDeviceAssetRepository {} - class MockSyncStreamRepository extends Mock implements SyncStreamRepository {} class MockLocalAlbumRepository extends Mock implements DriftLocalAlbumRepository {} diff --git a/mobile/test/modules/activity/activities_page_test.dart b/mobile/test/modules/activity/activities_page_test.dart deleted file mode 100644 index 39350530ea..0000000000 --- a/mobile/test/modules/activity/activities_page_test.dart +++ /dev/null @@ -1,175 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/pages/common/activities.page.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../fixtures/album.stub.dart'; -import '../../fixtures/asset.stub.dart'; -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../album/album_mocks.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; -import '../shared/shared_mocks.dart'; -import 'activity_mocks.dart'; - -final _activities = [ - Activity( - id: '1', - createdAt: DateTime(100), - type: ActivityType.comment, - comment: 'First Activity', - assetId: 'asset-2', - user: UserStub.admin, - ), - Activity( - id: '2', - createdAt: DateTime(200), - type: ActivityType.comment, - comment: 'Second Activity', - user: UserStub.user1, - ), - Activity(id: '3', createdAt: DateTime(300), type: ActivityType.like, assetId: 'asset-1', user: UserStub.user2), - Activity(id: '4', createdAt: DateTime(400), type: ActivityType.like, user: UserStub.user1), -]; - -void main() { - late MockAlbumActivity activityMock; - late MockCurrentAlbumProvider mockCurrentAlbumProvider; - late MockCurrentAssetProvider mockCurrentAssetProvider; - late List overrides; - late Isar db; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, UserStub.admin); - await Store.put(StoreKey.serverEndpoint, ''); - await Store.put(StoreKey.accessToken, ''); - }); - - setUp(() async { - mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset); - mockCurrentAssetProvider = MockCurrentAssetProvider(AssetStub.image1); - activityMock = MockAlbumActivity(_activities); - overrides = [ - albumActivityProvider(AlbumStub.twoAsset.remoteId!, AssetStub.image1.remoteId!).overrideWith(() => activityMock), - currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider), - currentAssetProvider.overrideWith(() => mockCurrentAssetProvider), - ]; - - await db.writeTxn(() async { - await db.clear(); - // Save all assets - await db.users.put(User.fromDto(UserStub.admin)); - await db.assets.putAll([AssetStub.image1, AssetStub.image2]); - await db.albums.put(AlbumStub.twoAsset); - await AlbumStub.twoAsset.owner.save(); - await AlbumStub.twoAsset.assets.save(); - }); - expect(db.albums.countSync(), 1); - expect(db.assets.countSync(), 2); - expect(db.users.countSync(), 1); - }); - - group("App bar", () { - testWidgets("No title when currentAsset != null", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - - final listTile = tester.widget(find.byType(AppBar)); - expect(listTile.title, isNull); - }); - - testWidgets("Album name as title when currentAsset == null", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - mockCurrentAssetProvider.state = null; - await tester.pumpAndSettle(); - - expect(find.text(AlbumStub.twoAsset.name), findsOneWidget); - final listTile = tester.widget(find.byType(AppBar)); - expect(listTile.title, isNotNull); - }); - }); - - group("Body", () { - testWidgets("Contains a stack with Activity List and Activity Input", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - expect(find.descendant(of: find.byType(Stack), matching: find.byType(ActivityTextField)), findsOneWidget); - - expect(find.descendant(of: find.byType(Stack), matching: find.byType(ListView)), findsOneWidget); - }); - - testWidgets("List Contains all dismissible activities", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - final listFinder = find.descendant(of: find.byType(Stack), matching: find.byType(ListView)); - final listChildren = find.descendant(of: listFinder, matching: find.byType(DismissibleActivity)); - expect(listChildren, findsNWidgets(_activities.length)); - }); - - testWidgets("Submitting text input adds a comment with the text", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - when(() => activityMock.addComment(any())).thenAnswer((_) => Future.value()); - - final textField = find.byType(TextField); - await tester.enterText(textField, 'Test comment'); - await tester.testTextInput.receiveAction(TextInputAction.done); - - verify(() => activityMock.addComment('Test comment')); - }); - - testWidgets("Owner can remove all activities", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - final deletableActivityFinder = find.byWidgetPredicate( - (widget) => widget is DismissibleActivity && widget.onDismiss != null, - ); - expect(deletableActivityFinder, findsNWidgets(_activities.length)); - }); - - testWidgets("Non-Owner can remove only their activities", (tester) async { - final mockCurrentUser = MockCurrentUserProvider(); - - await tester.pumpConsumerWidget( - const ActivitiesPage(), - overrides: [...overrides, currentUserProvider.overrideWith((ref) => mockCurrentUser)], - ); - mockCurrentUser.state = UserStub.user1; - await tester.pumpAndSettle(); - - final deletableActivityFinder = find.byWidgetPredicate( - (widget) => widget is DismissibleActivity && widget.onDismiss != null, - ); - expect(deletableActivityFinder, findsNWidgets(_activities.where((a) => a.user == UserStub.user1).length)); - }); - }); -} diff --git a/mobile/test/modules/activity/activity_mocks.dart b/mobile/test/modules/activity/activity_mocks.dart deleted file mode 100644 index c50810795e..0000000000 --- a/mobile/test/modules/activity/activity_mocks.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:immich_mobile/services/activity.service.dart'; -import 'package:mocktail/mocktail.dart'; - -class ActivityServiceMock extends Mock implements ActivityService {} - -class MockAlbumActivity extends AlbumActivityInternal with Mock implements AlbumActivity { - List? initActivities; - MockAlbumActivity([this.initActivities]); - - @override - Future> build(String albumId, [String? assetId]) async { - return initActivities ?? []; - } -} - -class ActivityStatisticsMock extends ActivityStatisticsInternal with Mock implements ActivityStatistics {} diff --git a/mobile/test/modules/activity/activity_provider_test.dart b/mobile/test/modules/activity/activity_provider_test.dart deleted file mode 100644 index 84eba62b70..0000000000 --- a/mobile/test/modules/activity/activity_provider_test.dart +++ /dev/null @@ -1,331 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import 'activity_mocks.dart'; - -final _activities = [ - Activity( - id: '1', - createdAt: DateTime(100), - type: ActivityType.comment, - comment: 'First Activity', - assetId: 'asset-2', - user: UserStub.admin, - ), - Activity( - id: '2', - createdAt: DateTime(200), - type: ActivityType.comment, - comment: 'Second Activity', - user: UserStub.user1, - ), - Activity(id: '3', createdAt: DateTime(300), type: ActivityType.like, assetId: 'asset-1', user: UserStub.admin), - Activity(id: '4', createdAt: DateTime(400), type: ActivityType.like, user: UserStub.user1), -]; - -void main() { - late ActivityServiceMock activityMock; - late ActivityStatisticsMock activityStatisticsMock; - late ActivityStatisticsMock albumActivityStatisticsMock; - late ProviderContainer container; - late AlbumActivityProvider provider; - late ListenerMock>> listener; - - setUpAll(() { - registerFallbackValue(AsyncData>([..._activities])); - }); - - setUp(() async { - activityMock = ActivityServiceMock(); - activityStatisticsMock = ActivityStatisticsMock(); - albumActivityStatisticsMock = ActivityStatisticsMock(); - - container = TestUtils.createContainer( - overrides: [ - activityServiceProvider.overrideWith((ref) => activityMock), - activityStatisticsProvider('test-album', 'test-asset').overrideWith(() => activityStatisticsMock), - activityStatisticsProvider('test-album').overrideWith(() => albumActivityStatisticsMock), - ], - ); - - // Mock values - when(() => activityStatisticsMock.build(any(), any())).thenReturn(0); - when(() => albumActivityStatisticsMock.build(any())).thenReturn(0); - when( - () => activityMock.getAllActivities('test-album', assetId: 'test-asset'), - ).thenAnswer((_) async => [..._activities]); - when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); - - // Init and wait for providers future to complete - provider = albumActivityProvider('test-album', 'test-asset'); - listener = ListenerMock(); - container.listen(provider, listener.call, fireImmediately: true); - - await container.read(provider.future); - }); - - test('Returns a list of activity', () async { - verifyInOrder([ - () => listener.call(null, const AsyncLoading()), - () => listener.call( - const AsyncLoading(), - any( - that: allOf([ - isA>>(), - predicate((AsyncData> ad) => ad.requireValue.every((e) => _activities.contains(e))), - ]), - ), - ), - ]); - - verifyNoMoreInteractions(listener); - }); - - group('addLike()', () { - test('Like successfully added', () async { - final like = Activity(id: '5', createdAt: DateTime(2023), type: ActivityType.like, user: UserStub.admin); - - when( - () => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'), - ).thenAnswer((_) async => AsyncData(like)); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).addLike(); - - verify(() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset')); - - final activities = await container.read(provider.future); - expect(activities, hasLength(5)); - expect(activities, contains(like)); - - // Never bump activity count for new likes - verifyNever(() => activityStatisticsMock.addActivity()); - verifyNever(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(5)); - expect(albumActivities, contains(like)); - }); - - test('Like failed', () async { - final like = Activity(id: '5', createdAt: DateTime(2023), type: ActivityType.like, user: UserStub.admin); - when( - () => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'), - ).thenAnswer((_) async => AsyncError(Exception('Mock'), StackTrace.current)); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).addLike(); - - verify(() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset')); - - final activities = await container.read(provider.future); - expect(activities, hasLength(4)); - expect(activities, isNot(contains(like))); - - verifyNever(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(4)); - expect(albumActivities, isNot(contains(like))); - }); - }); - - group('removeActivity()', () { - test('Like successfully removed', () async { - when(() => activityMock.removeActivity('3')).thenAnswer((_) async => true); - - await container.read(provider.notifier).removeActivity('3'); - - verify(() => activityMock.removeActivity('3')); - - final activities = await container.read(provider.future); - expect(activities, hasLength(3)); - expect(activities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); - }); - - test('Remove Like failed', () async { - when(() => activityMock.removeActivity('3')).thenAnswer((_) async => false); - - await container.read(provider.notifier).removeActivity('3'); - - final activities = await container.read(provider.future); - expect(activities, hasLength(4)); - expect(activities, anyElement(predicate((Activity a) => a.id == '3'))); - - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); - }); - - test('Comment successfully removed', () async { - when(() => activityMock.removeActivity('1')).thenAnswer((_) async => true); - - await container.read(provider.notifier).removeActivity('1'); - - final activities = await container.read(provider.future); - expect(activities, isNot(anyElement(predicate((Activity a) => a.id == '1')))); - - verify(() => activityStatisticsMock.removeActivity()); - verify(() => albumActivityStatisticsMock.removeActivity()); - }); - - test('Removes activity from album state when asset scoped', () async { - when(() => activityMock.removeActivity('3')).thenAnswer((_) async => true); - when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).removeActivity('3'); - - final assetActivities = container.read(provider).requireValue; - final albumActivities = container.read(albumProvider).requireValue; - - expect(assetActivities, hasLength(3)); - expect(assetActivities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - - expect(albumActivities, hasLength(3)); - expect(albumActivities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - - verify(() => activityMock.removeActivity('3')); - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); - }); - }); - - group('addComment()', () { - test('Comment successfully added', () async { - final comment = Activity( - id: '5', - createdAt: DateTime(2023), - type: ActivityType.comment, - user: UserStub.admin, - comment: 'Test-Comment', - assetId: 'test-asset', - ); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - when( - () => activityMock.addActivity( - 'test-album', - ActivityType.comment, - assetId: 'test-asset', - comment: 'Test-Comment', - ), - ).thenAnswer((_) async => AsyncData(comment)); - when(() => activityStatisticsMock.build('test-album', 'test-asset')).thenReturn(4); - when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2); - - await container.read(provider.notifier).addComment('Test-Comment'); - - verify( - () => activityMock.addActivity( - 'test-album', - ActivityType.comment, - assetId: 'test-asset', - comment: 'Test-Comment', - ), - ); - - final activities = await container.read(provider.future); - expect(activities, hasLength(5)); - expect(activities, contains(comment)); - - verify(() => activityStatisticsMock.addActivity()); - verify(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(5)); - expect(albumActivities, contains(comment)); - }); - - test('Comment successfully added without assetId', () async { - final comment = Activity( - id: '5', - createdAt: DateTime(2023), - type: ActivityType.comment, - user: UserStub.admin, - assetId: 'test-asset', - comment: 'Test-Comment', - ); - - when( - () => activityMock.addActivity('test-album', ActivityType.comment, comment: 'Test-Comment'), - ).thenAnswer((_) async => AsyncData(comment)); - when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2); - when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - await container.read(albumProvider.notifier).addComment('Test-Comment'); - - verify( - () => activityMock.addActivity('test-album', ActivityType.comment, assetId: null, comment: 'Test-Comment'), - ); - - final activities = await container.read(albumProvider.future); - expect(activities, hasLength(5)); - expect(activities, contains(comment)); - - verifyNever(() => activityStatisticsMock.addActivity()); - verify(() => albumActivityStatisticsMock.addActivity()); - }); - - test('Comment failed', () async { - final comment = Activity( - id: '5', - createdAt: DateTime(2023), - type: ActivityType.comment, - user: UserStub.admin, - comment: 'Test-Comment', - assetId: 'test-asset', - ); - - when( - () => activityMock.addActivity( - 'test-album', - ActivityType.comment, - assetId: 'test-asset', - comment: 'Test-Comment', - ), - ).thenAnswer((_) async => AsyncError(Exception('Error'), StackTrace.current)); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).addComment('Test-Comment'); - - final activities = await container.read(provider.future); - expect(activities, hasLength(4)); - expect(activities, isNot(contains(comment))); - - verifyNever(() => activityStatisticsMock.addActivity()); - verifyNever(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(4)); - expect(albumActivities, isNot(contains(comment))); - }); - }); -} diff --git a/mobile/test/modules/activity/activity_statistics_provider_test.dart b/mobile/test/modules/activity/activity_statistics_provider_test.dart deleted file mode 100644 index 7fe73868f5..0000000000 --- a/mobile/test/modules/activity/activity_statistics_provider_test.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../test_utils.dart'; -import 'activity_mocks.dart'; - -void main() { - late ActivityServiceMock activityMock; - late ProviderContainer container; - late ListenerMock listener; - - setUp(() async { - activityMock = ActivityServiceMock(); - container = TestUtils.createContainer(overrides: [activityServiceProvider.overrideWith((ref) => activityMock)]); - listener = ListenerMock(); - }); - - test('Returns the proper count family', () async { - when( - () => activityMock.getStatistics('test-album', assetId: 'test-asset'), - ).thenAnswer((_) async => const ActivityStats(comments: 5)); - - // Read here to make the getStatistics call - container.read(activityStatisticsProvider('test-album', 'test-asset')); - - container.listen(activityStatisticsProvider('test-album', 'test-asset'), listener.call, fireImmediately: true); - - // Sleep for the getStatistics future to resolve - await Future.delayed(const Duration(milliseconds: 1)); - - verifyInOrder([() => listener.call(null, 0), () => listener.call(0, 5)]); - - verifyNoMoreInteractions(listener); - }); - - test('Adds activity', () async { - when(() => activityMock.getStatistics('test-album')).thenAnswer((_) async => const ActivityStats(comments: 10)); - - final provider = activityStatisticsProvider('test-album'); - container.listen(provider, listener.call, fireImmediately: true); - - // Sleep for the getStatistics future to resolve - await Future.delayed(const Duration(milliseconds: 1)); - - container.read(provider.notifier).addActivity(); - container.read(provider.notifier).addActivity(); - - expect(container.read(provider), 12); - }); - - test('Removes activity', () async { - when( - () => activityMock.getStatistics('new-album', assetId: 'test-asset'), - ).thenAnswer((_) async => const ActivityStats(comments: 10)); - - final provider = activityStatisticsProvider('new-album', 'test-asset'); - container.listen(provider, listener.call, fireImmediately: true); - - // Sleep for the getStatistics future to resolve - await Future.delayed(const Duration(milliseconds: 1)); - - container.read(provider.notifier).removeActivity(); - container.read(provider.notifier).removeActivity(); - - expect(container.read(provider), 8); - }); -} diff --git a/mobile/test/modules/activity/activity_text_field_test.dart b/mobile/test/modules/activity/activity_text_field_test.dart deleted file mode 100644 index 4f4a2c7068..0000000000 --- a/mobile/test/modules/activity/activity_text_field_test.dart +++ /dev/null @@ -1,149 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../fixtures/album.stub.dart'; -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../album/album_mocks.dart'; -import '../shared/shared_mocks.dart'; -import 'activity_mocks.dart'; - -void main() { - late Isar db; - late MockCurrentAlbumProvider mockCurrentAlbumProvider; - late MockAlbumActivity activityMock; - late List overrides; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, UserStub.admin); - await Store.put(StoreKey.serverEndpoint, ''); - }); - - setUp(() { - mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset); - activityMock = MockAlbumActivity(); - overrides = [ - currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider), - albumActivityProvider(AlbumStub.twoAsset.remoteId!).overrideWith(() => activityMock), - ]; - }); - - testWidgets('Returns an Input text field', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - expect(find.byType(TextField), findsOneWidget); - }); - - testWidgets('No UserCircleAvatar when user == null', (tester) async { - final userProvider = MockCurrentUserProvider(); - - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (_) {}), - overrides: [currentUserProvider.overrideWith((ref) => userProvider), ...overrides], - ); - - expect(find.byType(UserCircleAvatar), findsNothing); - }); - - testWidgets('UserCircleAvatar displayed when user != null', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - expect(find.byType(UserCircleAvatar), findsOneWidget); - }); - - testWidgets('Filled icon if likedId != null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (_) {}, likeId: '1'), - overrides: overrides, - ); - - expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsOneWidget); - expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsNothing); - }); - - testWidgets('Bordered icon if likedId == null', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsOneWidget); - expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsNothing); - }); - - testWidgets('Adds new like', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - when(() => activityMock.addLike()).thenAnswer((_) => Future.value()); - - final suffixIcon = find.byType(IconButton); - await tester.tap(suffixIcon); - - verify(() => activityMock.addLike()); - }); - - testWidgets('Removes like if already liked', (tester) async { - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (_) {}, likeId: 'test-suffix'), - overrides: overrides, - ); - - when(() => activityMock.removeActivity(any())).thenAnswer((_) => Future.value()); - - final suffixIcon = find.byType(IconButton); - await tester.tap(suffixIcon); - - verify(() => activityMock.removeActivity('test-suffix')); - }); - - testWidgets('Passes text entered to onSubmit on submit', (tester) async { - String? receivedText; - - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (text) => receivedText = text, likeId: 'test-suffix'), - overrides: overrides, - ); - - final textField = find.byType(TextField); - await tester.enterText(textField, 'This is a test comment'); - await tester.testTextInput.receiveAction(TextInputAction.done); - expect(receivedText, 'This is a test comment'); - }); - - testWidgets('Input disabled when isEnabled false', (tester) async { - String? receviedText; - - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (text) => receviedText = text, isEnabled: false, likeId: 'test-suffix'), - overrides: overrides, - ); - - final suffixIcon = find.byType(IconButton); - await tester.tap(suffixIcon, warnIfMissed: false); - - final textField = find.byType(TextField); - await tester.enterText(textField, 'This is a test comment'); - await tester.testTextInput.receiveAction(TextInputAction.done); - - expect(receviedText, isNull); - verifyNever(() => activityMock.addLike()); - verifyNever(() => activityMock.removeActivity(any())); - }); -} diff --git a/mobile/test/modules/activity/activity_tile_test.dart b/mobile/test/modules/activity/activity_tile_test.dart deleted file mode 100644 index 538e3c0911..0000000000 --- a/mobile/test/modules/activity/activity_tile_test.dart +++ /dev/null @@ -1,165 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; -import 'package:isar/isar.dart'; - -import '../../fixtures/asset.stub.dart'; -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; - -void main() { - late MockCurrentAssetProvider assetProvider; - late List overrides; - late Isar db; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - // For UserCircleAvatar - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, UserStub.admin); - await Store.put(StoreKey.serverEndpoint, ''); - await Store.put(StoreKey.accessToken, ''); - }); - - setUp(() { - assetProvider = MockCurrentAssetProvider(); - overrides = [currentAssetProvider.overrideWith(() => assetProvider)]; - }); - - testWidgets('Returns a ListTile', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile(Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin)), - overrides: overrides, - ); - - expect(find.byType(ListTile), findsOneWidget); - }); - - testWidgets('No trailing widget when activity assetId == null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile(Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin)), - overrides: overrides, - ); - - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.trailing, isNull); - }); - - testWidgets('Asset Thumbanil as trailing widget when activity assetId != null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile( - Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin, assetId: '1'), - ), - overrides: overrides, - ); - - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.trailing, isNotNull); - // TODO: Validate this to be the common class after migrating ActivityTile#_ActivityAssetThumbnail to a common class - }); - - testWidgets('No trailing widget when current asset != null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile( - Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin, assetId: '1'), - ), - overrides: overrides, - ); - - assetProvider.state = AssetStub.image1; - await tester.pumpAndSettle(); - - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.trailing, isNull); - }); - - group('Like Activity', () { - final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin); - - testWidgets('Like contains filled thumbs-up as leading', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - // Leading widget should not be null - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.leading, isNotNull); - - // And should have a thumb_up icon - final thumbUpIconFinder = find.widgetWithIcon(listTile.leading!.runtimeType, Icons.thumb_up); - - expect(thumbUpIconFinder, findsOneWidget); - }); - - testWidgets('Like title is center aligned', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.titleAlignment, ListTileTitleAlignment.center); - }); - - testWidgets('No subtitle for likes', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.subtitle, isNull); - }); - }); - - group('Comment Activity', () { - final activity = Activity( - id: '1', - createdAt: DateTime(100), - type: ActivityType.comment, - comment: 'This is a test comment', - user: UserStub.admin, - ); - - testWidgets('Comment contains User Circle Avatar as leading', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final userAvatarFinder = find.byType(UserCircleAvatar); - expect(userAvatarFinder, findsOneWidget); - - // Leading widget should not be null - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.leading, isNotNull); - - // Make sure that the leading widget is the UserCircleAvatar - final userAvatar = tester.widget(userAvatarFinder); - expect(listTile.leading, userAvatar); - }); - - testWidgets('Comment title is top aligned', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.titleAlignment, ListTileTitleAlignment.top); - }); - - testWidgets('Contains comment text as subtitle', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.subtitle, isNotNull); - expect(find.descendant(of: find.byType(ListTile), matching: find.text(activity.comment!)), findsOneWidget); - }); - }); -} diff --git a/mobile/test/modules/activity/dismissible_activity_test.dart b/mobile/test/modules/activity/dismissible_activity_test.dart deleted file mode 100644 index 32516e73ea..0000000000 --- a/mobile/test/modules/activity/dismissible_activity_test.dart +++ /dev/null @@ -1,99 +0,0 @@ -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; - -final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin); - -void main() { - late MockCurrentAssetProvider assetProvider; - late List overrides; - - setUpAll(() => TestUtils.init()); - - setUp(() { - assetProvider = MockCurrentAssetProvider(); - overrides = [currentAssetProvider.overrideWith(() => assetProvider)]; - }); - - testWidgets('Returns a Dismissible', (tester) async { - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), - overrides: overrides, - ); - - expect(find.byType(Dismissible), findsOneWidget); - }); - - testWidgets('Dialog displayed when onDismiss is set', (tester) async { - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), - overrides: overrides, - ); - - final dismissible = find.byType(Dismissible); - await tester.drag(dismissible, const Offset(500, 0)); - await tester.pumpAndSettle(); - - expect(find.byType(ConfirmDialog), findsOneWidget); - }); - - testWidgets('Ok action in ConfirmDialog should call onDismiss with activityId', (tester) async { - String? receivedActivityId; - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (id) => receivedActivityId = id), - overrides: overrides, - ); - - final dismissible = find.byType(Dismissible); - await tester.drag(dismissible, const Offset(-500, 0)); - await tester.pumpAndSettle(); - - final okButton = find.text('delete'); - await tester.tap(okButton); - await tester.pumpAndSettle(); - - expect(receivedActivityId, '1'); - }); - - testWidgets('Delete icon for background if onDismiss is set', (tester) async { - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), - overrides: overrides, - ); - - final dismissible = find.byType(Dismissible); - await tester.drag(dismissible, const Offset(500, 0)); - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.delete_sweep_rounded), findsOneWidget); - }); - - testWidgets('No delete dialog if onDismiss is not set', (tester) async { - await tester.pumpConsumerWidget(DismissibleActivity('1', ActivityTile(activity)), overrides: overrides); - - // When onDismiss is not set, the widget should not be wrapped by a Dismissible - expect(find.byType(Dismissible), findsNothing); - expect(find.byType(ConfirmDialog), findsNothing); - }); - - testWidgets('No icon for background if onDismiss is not set', (tester) async { - await tester.pumpConsumerWidget(DismissibleActivity('1', ActivityTile(activity)), overrides: overrides); - - // No Dismissible should exist when onDismiss is not provided, so no delete icon either - expect(find.byType(Dismissible), findsNothing); - expect(find.byIcon(Icons.delete_sweep_rounded), findsNothing); - }); -} diff --git a/mobile/test/modules/album/album_mocks.dart b/mobile/test/modules/album/album_mocks.dart deleted file mode 100644 index 7a1b76e0c7..0000000000 --- a/mobile/test/modules/album/album_mocks.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCurrentAlbumProvider extends CurrentAlbum with Mock implements CurrentAlbumInternal { - Album? initAlbum; - MockCurrentAlbumProvider([this.initAlbum]); - - @override - Album? build() { - return initAlbum; - } -} diff --git a/mobile/test/modules/album/album_sort_by_options_provider_test.dart b/mobile/test/modules/album/album_sort_by_options_provider_test.dart deleted file mode 100644 index a35255bc21..0000000000 --- a/mobile/test/modules/album/album_sort_by_options_provider_test.dart +++ /dev/null @@ -1,270 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../fixtures/album.stub.dart'; -import '../../fixtures/asset.stub.dart'; -import '../../test_utils.dart'; -import '../settings/settings_mocks.dart'; - -void main() { - /// Verify the sort modes - group("AlbumSortMode", () { - late final Isar db; - - setUpAll(() async { - db = await TestUtils.initIsar(); - }); - - final albums = [AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset]; - - setUp(() { - db.writeTxnSync(() { - db.clearSync(); - // Save all assets - db.assets.putAllSync([AssetStub.image1, AssetStub.image2]); - db.albums.putAllSync(albums); - for (final album in albums) { - album.sharedUsers.saveSync(); - album.assets.saveSync(); - } - }); - expect(db.albums.countSync(), 4); - expect(db.assets.countSync(), 2); - }); - - group("Album sort - Created Time", () { - const created = AlbumSortMode.created; - test("Created time - ASC", () { - final sorted = created.sortFn(albums, false); - final sortedList = [AlbumStub.emptyAlbum, AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Created time - DESC", () { - final sorted = created.sortFn(albums, true); - final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset, AlbumStub.emptyAlbum]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Asset count", () { - const assetCount = AlbumSortMode.assetCount; - test("Asset Count - ASC", () { - final sorted = assetCount.sortFn(albums, false); - final sortedList = [AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Asset Count - DESC", () { - final sorted = assetCount.sortFn(albums, true); - final sortedList = [AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser, AlbumStub.emptyAlbum]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Last modified", () { - const lastModified = AlbumSortMode.lastModified; - test("Last modified - ASC", () { - final sorted = lastModified.sortFn(albums, false); - final sortedList = [AlbumStub.twoAsset, AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Last modified - DESC", () { - final sorted = lastModified.sortFn(albums, true); - final sortedList = [AlbumStub.oneAsset, AlbumStub.sharedWithUser, AlbumStub.emptyAlbum, AlbumStub.twoAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Created", () { - const created = AlbumSortMode.created; - test("Created - ASC", () { - final sorted = created.sortFn(albums, false); - final sortedList = [AlbumStub.emptyAlbum, AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Created - DESC", () { - final sorted = created.sortFn(albums, true); - final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset, AlbumStub.emptyAlbum]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Most Recent", () { - const mostRecent = AlbumSortMode.mostRecent; - - test("Most Recent - DESC", () { - final sorted = mostRecent.sortFn([ - AlbumStub.create2020end2020Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2026Album, - ], false); - final sortedList = [ - AlbumStub.create2020end2026Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2020Album, - ]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Most Recent - ASC", () { - final sorted = mostRecent.sortFn([ - AlbumStub.create2020end2020Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2026Album, - ], true); - final sortedList = [ - AlbumStub.create2020end2020Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2026Album, - ]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Most Oldest", () { - const mostOldest = AlbumSortMode.mostOldest; - - test("Most Oldest - ASC", () { - final sorted = mostOldest.sortFn(albums, false); - final sortedList = [AlbumStub.twoAsset, AlbumStub.emptyAlbum, AlbumStub.oneAsset, AlbumStub.sharedWithUser]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Most Oldest - DESC", () { - final sorted = mostOldest.sortFn(albums, true); - final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.emptyAlbum, AlbumStub.twoAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - }); - - /// Verify the sort mode provider - group('AlbumSortByOptions', () { - late AppSettingsService settingsMock; - late ProviderContainer container; - - setUp(() async { - settingsMock = MockAppSettingsService(); - container = TestUtils.createContainer( - overrides: [appSettingsServiceProvider.overrideWith((ref) => settingsMock)], - ); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, any()), - ).thenAnswer((_) async => {}); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, any()), - ).thenAnswer((_) async => {}); - }); - - test('Returns the default sort mode when none set', () { - // Returns the default value when nothing is set - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(0); - - expect(container.read(albumSortByOptionsProvider), AlbumSortMode.created); - }); - - test('Returns the correct sort mode with index from Store', () { - // Returns the default value when nothing is set - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(3); - - expect(container.read(albumSortByOptionsProvider), AlbumSortMode.lastModified); - }); - - test('Properly saves the correct store index of sort mode', () { - container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest); - - verify( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, AlbumSortMode.mostOldest.storeIndex), - ); - }); - - test('Notifies listeners on state change', () { - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(0); - - final listener = ListenerMock(); - container.listen(albumSortByOptionsProvider, listener.call, fireImmediately: true); - - // Created -> Most Oldest - container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest); - - // Most Oldest -> Title - container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.title); - - verifyInOrder([ - () => listener.call(null, AlbumSortMode.created), - () => listener.call(AlbumSortMode.created, AlbumSortMode.mostOldest), - () => listener.call(AlbumSortMode.mostOldest, AlbumSortMode.title), - ]); - - verifyNoMoreInteractions(listener); - }); - }); - - /// Verify the sort order provider - group('AlbumSortOrder', () { - late AppSettingsService settingsMock; - late ProviderContainer container; - - registerFallbackValue(AppSettingsEnum.selectedAlbumSortReverse); - - setUp(() async { - settingsMock = MockAppSettingsService(); - container = TestUtils.createContainer( - overrides: [appSettingsServiceProvider.overrideWith((ref) => settingsMock)], - ); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, any()), - ).thenAnswer((_) async => {}); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, any()), - ).thenAnswer((_) async => {}); - }); - - test('Returns the default sort order when none set - false', () { - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse)).thenReturn(false); - - expect(container.read(albumSortOrderProvider), isFalse); - }); - - test('Properly saves the correct order', () { - container.read(albumSortOrderProvider.notifier).changeSortDirection(true); - - verify(() => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, true)); - }); - - test('Notifies listeners on state change', () { - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse)).thenReturn(false); - - final listener = ListenerMock(); - container.listen(albumSortOrderProvider, listener.call, fireImmediately: true); - - // false -> true - container.read(albumSortOrderProvider.notifier).changeSortDirection(true); - - // true -> false - container.read(albumSortOrderProvider.notifier).changeSortDirection(false); - - verifyInOrder([ - () => listener.call(null, false), - () => listener.call(false, true), - () => listener.call(true, false), - ]); - - verifyNoMoreInteractions(listener); - }); - }); -} diff --git a/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart b/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart deleted file mode 100644 index 89b06d3c09..0000000000 --- a/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCurrentAssetProvider extends CurrentAssetInternal with Mock implements CurrentAsset { - Asset? initAsset; - MockCurrentAssetProvider([this.initAsset]); - - @override - Asset? build() { - return initAsset; - } -} diff --git a/mobile/test/modules/extensions/asset_extensions_test.dart b/mobile/test/modules/extensions/asset_extensions_test.dart deleted file mode 100644 index 2b9b740ca7..0000000000 --- a/mobile/test/modules/extensions/asset_extensions_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:timezone/data/latest.dart'; -import 'package:timezone/timezone.dart'; - -ExifInfo makeExif({DateTime? dateTimeOriginal, String? timeZone}) { - return ExifInfo(dateTimeOriginal: dateTimeOriginal, timeZone: timeZone); -} - -Asset makeAsset({required String id, required DateTime createdAt, ExifInfo? exifInfo}) { - return Asset( - checksum: '', - localId: id, - remoteId: id, - ownerId: 1, - fileCreatedAt: createdAt, - fileModifiedAt: DateTime.now(), - updatedAt: DateTime.now(), - durationInSeconds: 0, - type: AssetType.image, - fileName: id, - isFavorite: false, - isArchived: false, - isTrashed: false, - exifInfo: exifInfo, - ); -} - -void main() { - // Init Timezone DB - initializeTimeZones(); - - group("Returns local time and offset if no exifInfo", () { - test('returns createdAt directly if in local', () { - final createdAt = DateTime(2023, 12, 12, 12, 12, 12); - final a = makeAsset(id: '1', createdAt: createdAt); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - expect(createdAt, dt); - expect(createdAt.timeZoneOffset, tz); - }); - - test('returns createdAt in local if in utc', () { - final createdAt = DateTime.utc(2023, 12, 12, 12, 12, 12); - final a = makeAsset(id: '1', createdAt: createdAt); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final localCreatedAt = createdAt.toLocal(); - expect(localCreatedAt, dt); - expect(localCreatedAt.timeZoneOffset, tz); - }); - }); - - group("Returns dateTimeOriginal", () { - test('Returns dateTimeOriginal in UTC from exifInfo without timezone', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - final e = makeExif(dateTimeOriginal: dateTimeOriginal); - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final dateTimeInUTC = dateTimeOriginal.toUtc(); - expect(dateTimeInUTC, dt); - expect(dateTimeInUTC.timeZoneOffset, tz); - }); - - test('Returns dateTimeOriginal in UTC from exifInfo with invalid timezone', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: "#_#"); // Invalid timezone - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final dateTimeInUTC = dateTimeOriginal.toUtc(); - expect(dateTimeInUTC, dt); - expect(dateTimeInUTC.timeZoneOffset, tz); - }); - }); - - group("Returns adjusted time if timezone available", () { - test('With timezone as location', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - const location = "Asia/Hong_Kong"; - final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: location); - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final adjustedTime = TZDateTime.from(dateTimeOriginal.toUtc(), getLocation(location)); - expect(adjustedTime, dt); - expect(adjustedTime.timeZoneOffset, tz); - }); - - test('With timezone as offset', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - const offset = "utc+08:00"; - final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: offset); - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final location = getLocation("Asia/Hong_Kong"); - final offsetFromLocation = Duration(milliseconds: location.currentTimeZone.offset); - final adjustedTime = dateTimeOriginal.toUtc().add(offsetFromLocation); - - // Adds the offset to the actual time and returns the offset separately - expect(adjustedTime, dt); - expect(offsetFromLocation, tz); - }); - }); -} diff --git a/mobile/test/modules/home/asset_grid_data_structure_test.dart b/mobile/test/modules/home/asset_grid_data_structure_test.dart deleted file mode 100644 index 3e1fe06c68..0000000000 --- a/mobile/test/modules/home/asset_grid_data_structure_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -void main() { - final List testAssets = []; - - for (int i = 0; i < 150; i++) { - int month = i ~/ 31; - int day = (i % 31).toInt(); - - DateTime date = DateTime(2022, month, day); - - testAssets.add( - Asset( - checksum: "", - localId: '$i', - ownerId: 1, - fileCreatedAt: date, - fileModifiedAt: date, - updatedAt: date, - durationInSeconds: 0, - type: AssetType.image, - fileName: '', - isFavorite: false, - isArchived: false, - isTrashed: false, - ), - ); - } - - final List assets = []; - - assets.addAll( - testAssets.sublist(0, 5).map((e) { - e.fileCreatedAt = DateTime(2022, 1, 5); - return e; - }).toList(), - ); - assets.addAll( - testAssets.sublist(5, 10).map((e) { - e.fileCreatedAt = DateTime(2022, 1, 10); - return e; - }).toList(), - ); - assets.addAll( - testAssets.sublist(10, 15).map((e) { - e.fileCreatedAt = DateTime(2022, 2, 17); - return e; - }).toList(), - ); - assets.addAll( - testAssets.sublist(15, 30).map((e) { - e.fileCreatedAt = DateTime(2022, 10, 15); - return e; - }).toList(), - ); - - group('Test grouped', () { - test('test grouped check months', () async { - final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.day); - - // Oct - // Day 1 - // 15 Assets => 5 Rows - // Feb - // Day 1 - // 5 Assets => 2 Rows - // Jan - // Day 2 - // 5 Assets => 2 Rows - // Day 1 - // 5 Assets => 2 Rows - expect(renderList.elements, hasLength(4)); - expect(renderList.elements[0].type, RenderAssetGridElementType.monthTitle); - expect(renderList.elements[0].date.month, 1); - expect(renderList.elements[1].type, RenderAssetGridElementType.groupDividerTitle); - expect(renderList.elements[1].date.month, 1); - expect(renderList.elements[2].type, RenderAssetGridElementType.monthTitle); - expect(renderList.elements[2].date.month, 2); - expect(renderList.elements[3].type, RenderAssetGridElementType.monthTitle); - expect(renderList.elements[3].date.month, 10); - }); - - test('test grouped check types', () async { - final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.day); - - // Oct - // Day 1 - // 15 Assets => 3 Rows - // Feb - // Day 1 - // 5 Assets => 1 Row - // Jan - // Day 2 - // 5 Assets => 1 Row - // Day 1 - // 5 Assets => 1 Row - final types = [ - RenderAssetGridElementType.monthTitle, - RenderAssetGridElementType.groupDividerTitle, - RenderAssetGridElementType.monthTitle, - RenderAssetGridElementType.monthTitle, - ]; - - expect(renderList.elements, hasLength(types.length)); - - for (int i = 0; i < renderList.elements.length; i++) { - expect(renderList.elements[i].type, types[i]); - } - }); - }); -} diff --git a/mobile/test/modules/map/map_theme_override_test.dart b/mobile/test/modules/map/map_theme_override_test.dart index de16b7f24f..56efde98dd 100644 --- a/mobile/test/modules/map/map_theme_override_test.dart +++ b/mobile/test/modules/map/map_theme_override_test.dart @@ -2,16 +2,18 @@ @Tags(['widget']) library; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/models/map/map_state.model.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart'; -import 'package:isar/isar.dart'; import '../../test_utils.dart'; import '../../widget_tester_extensions.dart'; @@ -21,17 +23,17 @@ void main() { late MockMapStateNotifier mapStateNotifier; late List overrides; late MapState mapState; - late Isar db; + late Drift db; setUpAll(() async { - db = await TestUtils.initIsar(); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); TestUtils.init(); }); setUp(() async { mapState = const MapState(themeMode: ThemeMode.dark); mapStateNotifier = MockMapStateNotifier(mapState); - await StoreService.init(storeRepository: IsarStoreRepository(db)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); overrides = [ mapStateNotifierProvider.overrideWith(() => mapStateNotifier), localeProvider.overrideWithValue(const Locale("en")), diff --git a/mobile/test/modules/settings/settings_mocks.dart b/mobile/test/modules/settings/settings_mocks.dart deleted file mode 100644 index 63fd9312b7..0000000000 --- a/mobile/test/modules/settings/settings_mocks.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockAppSettingsService extends Mock implements AppSettingsService {} diff --git a/mobile/test/modules/shared/shared_mocks.dart b/mobile/test/modules/shared/shared_mocks.dart deleted file mode 100644 index 790bbbd815..0000000000 --- a/mobile/test/modules/shared/shared_mocks.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCurrentUserProvider extends StateNotifier with Mock implements CurrentUserProvider { - MockCurrentUserProvider() : super(null); - - @override - set state(UserDto? user) => super.state = user; -} diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart deleted file mode 100644 index 767a52b8d8..0000000000 --- a/mobile/test/modules/shared/sync_service_test.dart +++ /dev/null @@ -1,285 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/log.service.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../domain/service.mock.dart'; -import '../../fixtures/asset.stub.dart'; -import '../../infrastructure/repository.mock.dart'; -import '../../repository.mocks.dart'; -import '../../service.mocks.dart'; -import '../../test_utils.dart'; - -void main() { - int assetIdCounter = 0; - Asset makeAsset({ - required String checksum, - String? localId, - String? remoteId, - int ownerId = 590700560494856554, // hash of "1" - }) { - final DateTime date = DateTime(2000); - return Asset( - id: assetIdCounter++, - checksum: checksum, - localId: localId, - remoteId: remoteId, - ownerId: ownerId, - fileCreatedAt: date, - fileModifiedAt: date, - updatedAt: date, - durationInSeconds: 0, - type: AssetType.image, - fileName: localId ?? remoteId ?? "", - isFavorite: false, - isArchived: false, - isTrashed: false, - ); - } - - final owner = UserDto( - id: "1", - updatedAt: DateTime.now(), - email: "a@b.c", - name: "first last", - isAdmin: false, - profileChangedAt: DateTime.now(), - ); - - setUpAll(() async { - final loggerDb = DriftLogger(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - final LogRepository logRepository = LogRepository(loggerDb); - - WidgetsFlutterBinding.ensureInitialized(); - final db = await TestUtils.initIsar(); - - db.writeTxnSync(() => db.clearSync()); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, owner); - await LogService.init(logRepository: logRepository, storeRepository: IsarStoreRepository(db)); - }); - - group('Test SyncService grouped', () { - final MockHashService hs = MockHashService(); - final MockEntityService entityService = MockEntityService(); - final MockAlbumRepository albumRepository = MockAlbumRepository(); - final MockAssetRepository assetRepository = MockAssetRepository(); - final MockExifInfoRepository exifInfoRepository = MockExifInfoRepository(); - final MockIsarUserRepository userRepository = MockIsarUserRepository(); - final MockETagRepository eTagRepository = MockETagRepository(); - final MockAlbumMediaRepository albumMediaRepository = MockAlbumMediaRepository(); - final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository(); - final MockAppSettingService appSettingService = MockAppSettingService(); - final MockLocalFilesManagerRepository localFilesManagerRepository = MockLocalFilesManagerRepository(); - final MockPartnerApiRepository partnerApiRepository = MockPartnerApiRepository(); - final MockUserApiRepository userApiRepository = MockUserApiRepository(); - final MockPartnerRepository partnerRepository = MockPartnerRepository(); - final MockUserService userService = MockUserService(); - - final owner = UserDto( - id: "1", - updatedAt: DateTime.now(), - email: "a@b.c", - name: "first last", - isAdmin: false, - profileChangedAt: DateTime(2021), - ); - - late SyncService s; - - final List initialAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "2-1"), - makeAsset(checksum: "c", localId: "1", remoteId: "1-1"), - makeAsset(checksum: "d", localId: "2"), - makeAsset(checksum: "e", localId: "3"), - ]; - setUp(() { - s = SyncService( - hs, - entityService, - albumMediaRepository, - albumApiRepository, - albumRepository, - assetRepository, - exifInfoRepository, - partnerRepository, - userRepository, - userService, - eTagRepository, - appSettingService, - localFilesManagerRepository, - partnerApiRepository, - userApiRepository, - ); - when(() => userService.getMyUser()).thenReturn(owner); - when(() => eTagRepository.get(owner.id)).thenAnswer((_) async => ETag(id: owner.id, time: DateTime.now())); - when(() => eTagRepository.deleteByIds(["1"])).thenAnswer((_) async {}); - when(() => eTagRepository.upsertAll(any())).thenAnswer((_) async {}); - when(() => partnerRepository.getSharedWith()).thenAnswer((_) async => []); - when(() => userRepository.getAll(sortBy: SortUserBy.id)).thenAnswer((_) async => [owner]); - when(() => userRepository.getAll()).thenAnswer((_) async => [owner]); - when( - () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum), - ).thenAnswer((_) async => initialAssets); - when( - () => assetRepository.getAllByOwnerIdChecksum(any(), any()), - ).thenAnswer((_) async => [initialAssets[3], null, null]); - when(() => assetRepository.updateAll(any())).thenAnswer((_) async => []); - when(() => assetRepository.deleteByIds(any())).thenAnswer((_) async {}); - when(() => exifInfoRepository.updateAll(any())).thenAnswer((_) async => []); - when( - () => assetRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - when( - () => assetRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - when(() => userApiRepository.getAll()).thenAnswer((_) async => [owner]); - registerFallbackValue(Direction.sharedByMe); - when(() => partnerApiRepository.getAll(any())).thenAnswer((_) async => []); - }); - test('test inserting existing assets', () async { - final List remoteAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "2-1"), - makeAsset(checksum: "c", remoteId: "1-1"), - ]; - final bool c1 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c1, isFalse); - verifyNever(() => assetRepository.updateAll(any())); - }); - - test('test inserting new assets', () async { - final List remoteAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "2-1"), - makeAsset(checksum: "c", remoteId: "1-1"), - makeAsset(checksum: "d", remoteId: "1-2"), - makeAsset(checksum: "f", remoteId: "1-4"), - makeAsset(checksum: "g", remoteId: "3-1"), - ]; - final bool c1 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c1, isTrue); - final updatedAsset = initialAssets[3].updatedCopy(remoteAssets[3]); - verify(() => assetRepository.updateAll([remoteAssets[4], remoteAssets[5], updatedAsset])); - }); - - test('test syncing duplicate assets', () async { - final List remoteAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "1-1"), - makeAsset(checksum: "c", remoteId: "2-1"), - makeAsset(checksum: "h", remoteId: "2-1b"), - makeAsset(checksum: "i", remoteId: "2-1c"), - makeAsset(checksum: "j", remoteId: "2-1d"), - ]; - final bool c1 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c1, isTrue); - when( - () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum), - ).thenAnswer((_) async => remoteAssets); - final bool c2 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c2, isFalse); - final currentState = [...remoteAssets]; - when( - () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum), - ).thenAnswer((_) async => currentState); - remoteAssets.removeAt(4); - final bool c3 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c3, isTrue); - remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e")); - remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2")); - final bool c4 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c4, isTrue); - }); - - test('test efficient sync', () async { - when( - () => assetRepository.deleteAllByRemoteId([ - initialAssets[1].remoteId!, - initialAssets[2].remoteId!, - ], state: AssetState.remote), - ).thenAnswer((_) async { - return; - }); - when( - () => assetRepository.getAllByRemoteId(["2-1", "1-1"], state: AssetState.merged), - ).thenAnswer((_) async => [initialAssets[2]]); - when( - () => assetRepository.getAllByOwnerIdChecksum(any(), any()), - ).thenAnswer((_) async => [initialAssets[0], null, null]); //afg - final List toUpsert = [ - makeAsset(checksum: "a", remoteId: "0-1"), // changed - makeAsset(checksum: "f", remoteId: "0-2"), // new - makeAsset(checksum: "g", remoteId: "0-3"), // new - ]; - toUpsert[0].isFavorite = true; - final List toDelete = ["2-1", "1-1"]; - final expected = [...toUpsert]; - expected[0].id = initialAssets[0].id; - final bool c = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: (user, since) async => (toUpsert, toDelete), - loadAssets: (user, date) => throw Exception(), - ); - expect(c, isTrue); - verify(() => assetRepository.updateAll(expected)); - }); - - group("upsertAssetsWithExif", () { - test('test upsert with EXIF data', () async { - final assets = [AssetStub.image1, AssetStub.image2]; - - expect(assets.map((a) => a.exifInfo?.assetId), List.filled(assets.length, null)); - await s.upsertAssetsWithExif(assets); - verify( - () => exifInfoRepository.updateAll( - any(that: containsAll(assets.map((a) => a.exifInfo!.copyWith(assetId: a.id)))), - ), - ); - expect(assets.map((a) => a.exifInfo?.assetId), assets.map((a) => a.id)); - }); - }); - }); -} - -Future<(List?, List?)> _failDiff(List user, DateTime time) => Future.value((null, null)); diff --git a/mobile/test/modules/utils/migration_test.dart b/mobile/test/modules/utils/migration_test.dart deleted file mode 100644 index 08ab1204a6..0000000000 --- a/mobile/test/modules/utils/migration_test.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'package:drift/drift.dart' hide isNull; -import 'package:drift/native.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; -import 'package:immich_mobile/utils/migration.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; - -void main() { - late Drift db; - late SyncStreamRepository mockSyncStreamRepository; - - setUpAll(() async { - db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: DriftStoreRepository(db)); - mockSyncStreamRepository = MockSyncStreamRepository(); - when(() => mockSyncStreamRepository.reset()).thenAnswer((_) async => {}); - }); - - tearDown(() async { - await Store.clear(); - }); - - group('handleBetaMigration Tests', () { - group("version < 15", () { - test('already on new timeline', () async { - await Store.put(StoreKey.betaTimeline, true); - - await handleBetaMigration(14, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('already on old timeline', () async { - await Store.put(StoreKey.betaTimeline, false); - - await handleBetaMigration(14, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.needBetaMigration), true); - }); - - test('fresh install', () async { - await Store.delete(StoreKey.betaTimeline); - await handleBetaMigration(14, true, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - }); - - group("version == 15", () { - test('already on new timeline', () async { - await Store.put(StoreKey.betaTimeline, true); - - await handleBetaMigration(15, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('already on old timeline', () async { - await Store.put(StoreKey.betaTimeline, false); - - await handleBetaMigration(15, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.needBetaMigration), true); - }); - - test('fresh install', () async { - await Store.delete(StoreKey.betaTimeline); - await handleBetaMigration(15, true, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - }); - - group("version > 15", () { - test('already on new timeline', () async { - await Store.put(StoreKey.betaTimeline, true); - - await handleBetaMigration(16, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('already on old timeline', () async { - await Store.put(StoreKey.betaTimeline, false); - - await handleBetaMigration(16, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), false); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('fresh install', () async { - await Store.delete(StoreKey.betaTimeline); - await handleBetaMigration(16, true, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - }); - }); - - group('sync reset tests', () { - test('version < 16', () async { - await Store.put(StoreKey.shouldResetSync, false); - - await handleBetaMigration(15, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.shouldResetSync), true); - }); - - test('version >= 16', () async { - await Store.put(StoreKey.shouldResetSync, false); - - await handleBetaMigration(16, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.shouldResetSync), false); - }); - }); -} diff --git a/mobile/test/modules/utils/openapi_patching_test.dart b/mobile/test/modules/utils/openapi_patching_test.dart index a577b0544f..18ab07b3a9 100644 --- a/mobile/test/modules/utils/openapi_patching_test.dart +++ b/mobile/test/modules/utils/openapi_patching_test.dart @@ -21,7 +21,7 @@ void main() { """); upgradeDto(value, targetType); - expect(value['tags'], TagsResponse().toJson()); + expect(value['tags'], TagsResponse(enabled: false, sidebarWeb: false).toJson()); expect(value['download']['includeEmbeddedVideos'], false); }); diff --git a/mobile/test/modules/utils/throttler_test.dart b/mobile/test/modules/utils/throttler_test.dart deleted file mode 100644 index 1757826daf..0000000000 --- a/mobile/test/modules/utils/throttler_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -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; - _Counter(); - - int get count => _count; - void increment() { - dPrint(() => "Counter inside increment: $count"); - _count = _count + 1; - } -} - -void main() { - test('Executes the method immediately if no calls received previously', () async { - var counter = _Counter(); - final throttler = Throttler(interval: const Duration(milliseconds: 300)); - throttler.run(() => counter.increment()); - expect(counter.count, 1); - }); - - test('Does not execute calls before throttle interval', () async { - var counter = _Counter(); - final throttler = Throttler(interval: const Duration(milliseconds: 100)); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - await Future.delayed(const Duration(seconds: 1)); - expect(counter.count, 1); - }); - - test('Executes the method if received in intervals', () async { - var counter = _Counter(); - final throttler = Throttler(interval: const Duration(milliseconds: 100)); - for (final _ in Iterable.generate(10)) { - throttler.run(() => counter.increment()); - await Future.delayed(const Duration(milliseconds: 50)); - } - await Future.delayed(const Duration(seconds: 1)); - expect(counter.count, 5); - }); -} diff --git a/mobile/test/modules/utils/thumbnail_utils_test.dart b/mobile/test/modules/utils/thumbnail_utils_test.dart deleted file mode 100644 index dd4588fc80..0000000000 --- a/mobile/test/modules/utils/thumbnail_utils_test.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/thumbnail_utils.dart'; - -void main() { - final dateTime = DateTime(2025, 04, 25, 12, 13, 14); - final dateTimeString = DateFormat.yMMMMd().format(dateTime); - - test('returns description if it has one', () { - final result = getAltText(const ExifInfo(description: 'description'), dateTime, AssetType.image, []); - expect(result, 'description'); - }); - - test('returns image alt text with date if no location', () { - final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, []); - expect(template, "image_alt_text_date"); - expect(args["isVideo"], "false"); - expect(args["date"], dateTimeString); - }); - - test('returns image alt text with date and place', () { - final (template, args) = getAltTextTemplate( - const ExifInfo(city: 'city', country: 'country'), - dateTime, - AssetType.video, - [], - ); - expect(template, "image_alt_text_date_place"); - expect(args["isVideo"], "true"); - expect(args["date"], dateTimeString); - expect(args["city"], "city"); - expect(args["country"], "country"); - }); - - test('returns image alt text with date and some people', () { - final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, ["Alice", "Bob"]); - expect(template, "image_alt_text_date_2_people"); - expect(args["isVideo"], "false"); - expect(args["date"], dateTimeString); - expect(args["person1"], "Alice"); - expect(args["person2"], "Bob"); - }); - - test('returns image alt text with date and location and many people', () { - final (template, args) = getAltTextTemplate( - const ExifInfo(city: "city", country: 'country'), - dateTime, - AssetType.video, - ["Alice", "Bob", "Carol", "David", "Eve"], - ); - expect(template, "image_alt_text_date_place_4_or_more_people"); - expect(args["isVideo"], "true"); - expect(args["date"], dateTimeString); - expect(args["city"], "city"); - expect(args["country"], "country"); - expect(args["person1"], "Alice"); - expect(args["person2"], "Bob"); - expect(args["person3"], "Carol"); - expect(args["additionalCount"], "2"); - }); -} diff --git a/mobile/test/pages/search/search.page_test.dart b/mobile/test/pages/search/search.page_test.dart deleted file mode 100644 index 9592623a28..0000000000 --- a/mobile/test/pages/search/search.page_test.dart +++ /dev/null @@ -1,98 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['pages']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/pages/search/search.page.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; - -import '../../dto.mocks.dart'; -import '../../service.mocks.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; - -void main() { - late List overrides; - late Isar db; - late MockApiService mockApiService; - late MockSearchApi mockSearchApi; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - mockApiService = MockApiService(); - mockSearchApi = MockSearchApi(); - when(() => mockApiService.searchApi).thenReturn(mockSearchApi); - registerFallbackValue(MockSmartSearchDto()); - registerFallbackValue(MockMetadataSearchDto()); - overrides = [ - dbProvider.overrideWithValue(db), - isarProvider.overrideWithValue(db), - apiServiceProvider.overrideWithValue(mockApiService), - ]; - }); - - final emptyTextSearch = isA().having((s) => s.originalFileName, 'originalFileName', null); - - testWidgets('contextual search with/without text', (tester) async { - await tester.pumpConsumerWidget(const SearchPage(), overrides: overrides); - - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.abc_rounded), findsOneWidget, reason: 'Should have contextual search icon'); - - final searchField = find.byKey(const Key('search_text_field')); - expect(searchField, findsOneWidget); - - await tester.enterText(searchField, 'test'); - await tester.testTextInput.receiveAction(TextInputAction.search); - - var captured = verify(() => mockSearchApi.searchSmart(captureAny())).captured; - - expect(captured.first, isA().having((s) => s.query, 'query', 'test')); - - await tester.enterText(searchField, ''); - await tester.testTextInput.receiveAction(TextInputAction.search); - - captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured; - expect(captured.first, emptyTextSearch); - }); - - testWidgets('not contextual search with/without text', (tester) async { - await tester.pumpConsumerWidget(const SearchPage(), overrides: overrides); - - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key('contextual_search_button'))); - - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.image_search_rounded), findsOneWidget, reason: 'Should not have contextual search icon'); - - final searchField = find.byKey(const Key('search_text_field')); - expect(searchField, findsOneWidget); - - await tester.enterText(searchField, 'test'); - await tester.testTextInput.receiveAction(TextInputAction.search); - - var captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured; - - expect(captured.first, isA().having((s) => s.originalFileName, 'originalFileName', 'test')); - - await tester.enterText(searchField, ''); - await tester.testTextInput.receiveAction(TextInputAction.search); - - captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured; - expect(captured.first, emptyTextSearch); - }); -} diff --git a/mobile/test/presentation/widgets/images/cache_aware_listener_tracker_mixin_test.dart b/mobile/test/presentation/widgets/images/cache_aware_listener_tracker_mixin_test.dart new file mode 100644 index 0000000000..02bb0c1053 --- /dev/null +++ b/mobile/test/presentation/widgets/images/cache_aware_listener_tracker_mixin_test.dart @@ -0,0 +1,183 @@ +import 'dart:ui' as ui; + +import 'package:flutter/painting.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/presentation/widgets/images/cache_aware_listener_tracker.mixin.dart'; + +class TestImageCompleter extends ImageStreamCompleter with CacheAwareListenerTrackerMixin { + bool wasCancelled = false; + + TestImageCompleter({required bool hadInitialImage}) { + setupListenerTracking( + hadInitialImage: hadInitialImage, + onLastListenerRemoved: () { + wasCancelled = true; + }, + ); + } + + @override + void setImage(ImageInfo image) { + super.setImage(image); + } +} + +void main() { + late ImageCache cache; + late ImageStreamListener uiListener; + + setUp(() { + // Create a fresh, real Flutter ImageCache for every test + cache = ImageCache(); + uiListener = ImageStreamListener((_, __) {}); + }); + + group('CacheAwareListenerTrackerMixin with Real ImageCache', () { + + testWidgets('cancels fetch when UI detaches before completion', (WidgetTester tester) async { + final completer = TestImageCompleter(hadInitialImage: false); + final key = Object(); + + // 1. Request image from the real cache (simulating the provider) + final stream = cache.putIfAbsent(key, () => completer)!; + + // 2. UI attaches + stream.addListener(uiListener); + expect(completer.wasCancelled, isFalse); + + // 3. Simulate asynchronous network delay... + await tester.pump(const Duration(milliseconds: 150)); + + // 4. User scrolls away before network finishes. UI detaches. + stream.removeListener(uiListener); + + expect(completer.wasCancelled, isTrue); + }); + + testWidgets('survives cache eviction while UI listener is still attached', (WidgetTester tester) async { + final completer = TestImageCompleter(hadInitialImage: false); + final key = Object(); + + // 1. Request image and attach UI + final stream = cache.putIfAbsent(key, () => completer)!; + stream.addListener(uiListener); + + // 2. Simulate app going to background -> OS Memory Warning -> Cache clears + cache.clear(); + + // Even though the real cache just aggressively detached its listener, + // the stream MUST survive because the UI widget is still on screen! + expect(completer.wasCancelled, isFalse); + + // 3. UI widget finally detaches + stream.removeListener(uiListener); + expect(completer.wasCancelled, isTrue); + }); + + testWidgets('survives synchronous cache detach during putIfAbsent with initialImage', (WidgetTester tester) async { + final completer = TestImageCompleter(hadInitialImage: true); + final key = Object(); + + // Run image creation outside FakeAsync zone to avoid hang + late ui.Image dummyImage; + await tester.runAsync(() async { + dummyImage = await createTestImage(width: 1, height: 1); + }); + + final initialImageInfo = ImageInfo(image: dummyImage); + + final stream = cache.putIfAbsent(key, () { + completer.setImage(initialImageInfo); + return completer; + })!; + + expect(completer.wasCancelled, isFalse); + + stream.addListener(uiListener); + expect(completer.wasCancelled, isFalse); + + stream.removeListener(uiListener); + expect(completer.wasCancelled, isTrue); + }); + + testWidgets('fires cleanup on full abandonment even after successful fetch', (WidgetTester tester) async { + final completer = TestImageCompleter(hadInitialImage: false); + final key = Object(); + + final stream = cache.putIfAbsent(key, () => completer)!; + stream.addListener(uiListener); + + await tester.pump(const Duration(milliseconds: 100)); + + // Run image creation outside FakeAsync zone to avoid hang + late ui.Image dummyImage; + await tester.runAsync(() async { + dummyImage = await createTestImage(width: 1, height: 1); + }); + + completer.setImage(ImageInfo(image: dummyImage)); + + stream.removeListener(uiListener); + + // The stream is completely abandoned (0 listeners), so it fires the cleanup hook. + // Since the image is already downloaded, canceling the network token is a safe no-op. + expect(completer.wasCancelled, isTrue); + }); + + testWidgets('Multiple UI listeners — only all detached, should cancel', (WidgetTester tester) async { + final completer = TestImageCompleter(hadInitialImage: false); + final key = Object(); + + final stream = cache.putIfAbsent(key, () => completer)!; + + final uiListener2 = ImageStreamListener((_, __) {}); + stream.addListener(uiListener); + stream.addListener(uiListener2); + + // First UI detach leaves cache + one UI → no cancel + stream.removeListener(uiListener); + expect(completer.wasCancelled, isFalse); + + // Second UI detach leaves only cache → cancel + stream.removeListener(uiListener2); + expect(completer.wasCancelled, isTrue); + }); + + testWidgets('Listener misidentification: new listener after cache eviction is not treated as cache', (WidgetTester tester) async { + final completer = TestImageCompleter(hadInitialImage: false); + final key = Object(); + + final stream = cache.putIfAbsent(key, () => completer)!; + + // UI attaches + stream.addListener(uiListener); + + // Cache eviction removes the cache listener + cache.clear(); + expect(completer.wasCancelled, isFalse); + + // A second UI listener attaches — must NOT be treated as cache + final uiListener2 = ImageStreamListener((_, __) {}); + stream.addListener(uiListener2); + + // Remove first UI listener; second UI still active → no cancel + stream.removeListener(uiListener); + expect(completer.wasCancelled, isFalse); + + // Remove second UI listener; completely abandoned → cancel + stream.removeListener(uiListener2); + expect(completer.wasCancelled, isTrue); + }); + + testWidgets('No UI listener ever attaches (cache-only) — cache detaches should cancel', (WidgetTester tester) async { + final completer = TestImageCompleter(hadInitialImage: false); + final key = Object(); + + cache.putIfAbsent(key, () => completer); + + // Cache eviction removes the only listener + cache.clear(); + expect(completer.wasCancelled, isTrue); + }); + }); +} diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart index 4b54ec4055..d049626f1d 100644 --- a/mobile/test/repository.mocks.dart +++ b/mobile/test/repository.mocks.dart @@ -1,48 +1,16 @@ -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; -import 'package:immich_mobile/repositories/partner.repository.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; +import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/auth.repository.dart'; import 'package:immich_mobile/repositories/auth_api.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/album.repository.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:mocktail/mocktail.dart'; -class MockAlbumRepository extends Mock implements AlbumRepository {} - -class MockAssetRepository extends Mock implements AssetRepository {} - -class MockBackupRepository extends Mock implements BackupAlbumRepository {} - -class MockExifInfoRepository extends Mock implements IsarExifRepository {} - -class MockETagRepository extends Mock implements ETagRepository {} - -class MockAlbumMediaRepository extends Mock implements AlbumMediaRepository {} - -class MockBackupAlbumRepository extends Mock implements BackupAlbumRepository {} - class MockAssetApiRepository extends Mock implements AssetApiRepository {} class MockAssetMediaRepository extends Mock implements AssetMediaRepository {} -class MockFileMediaRepository extends Mock implements FileMediaRepository {} - -class MockAlbumApiRepository extends Mock implements AlbumApiRepository {} - class MockAuthApiRepository extends Mock implements AuthApiRepository {} class MockAuthRepository extends Mock implements AuthRepository {} -class MockPartnerRepository extends Mock implements PartnerRepository {} - -class MockPartnerApiRepository extends Mock implements PartnerApiRepository {} - class MockLocalFilesManagerRepository extends Mock implements LocalFilesManagerRepository {} diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index 87a8c01cf0..4591dd845d 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -1,31 +1,10 @@ -import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/services/network.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; class MockApiService extends Mock implements ApiService {} -class MockAlbumService extends Mock implements AlbumService {} - -class MockBackupService extends Mock implements BackupService {} - -class MockSyncService extends Mock implements SyncService {} - -class MockHashService extends Mock implements HashService {} - -class MockEntityService extends Mock implements EntityService {} - class MockNetworkService extends Mock implements NetworkService {} -class MockSearchApi extends Mock implements SearchApi {} - class MockAppSettingService extends Mock implements AppSettingsService {} - -class MockBackgroundService extends Mock implements BackgroundService {} diff --git a/mobile/test/services/album.service_test.dart b/mobile/test/services/album.service_test.dart deleted file mode 100644 index 97683cdab1..0000000000 --- a/mobile/test/services/album.service_test.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../domain/service.mock.dart'; -import '../fixtures/album.stub.dart'; -import '../fixtures/asset.stub.dart'; -import '../fixtures/user.stub.dart'; -import '../repository.mocks.dart'; -import '../service.mocks.dart'; - -void main() { - late AlbumService sut; - late MockUserService userService; - late MockSyncService syncService; - late MockEntityService entityService; - late MockAlbumRepository albumRepository; - late MockAssetRepository assetRepository; - late MockBackupRepository backupRepository; - late MockAlbumMediaRepository albumMediaRepository; - late MockAlbumApiRepository albumApiRepository; - - setUp(() { - userService = MockUserService(); - syncService = MockSyncService(); - entityService = MockEntityService(); - albumRepository = MockAlbumRepository(); - assetRepository = MockAssetRepository(); - backupRepository = MockBackupRepository(); - albumMediaRepository = MockAlbumMediaRepository(); - albumApiRepository = MockAlbumApiRepository(); - - when(() => userService.getMyUser()).thenReturn(UserStub.user1); - - when( - () => albumRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - when( - () => assetRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - - sut = AlbumService( - syncService, - userService, - entityService, - albumRepository, - assetRepository, - backupRepository, - albumMediaRepository, - albumApiRepository, - ); - }); - - group('refreshDeviceAlbums', () { - test('empty selection with one album in db', () async { - when(() => backupRepository.getIdsBySelection(BackupSelection.exclude)).thenAnswer((_) async => []); - when(() => backupRepository.getIdsBySelection(BackupSelection.select)).thenAnswer((_) async => []); - when(() => albumMediaRepository.getAll()).thenAnswer((_) async => []); - when(() => albumRepository.count(local: true)).thenAnswer((_) async => 1); - when(() => syncService.removeAllLocalAlbumsAndAssets()).thenAnswer((_) async => true); - final result = await sut.refreshDeviceAlbums(); - expect(result, false); - verify(() => syncService.removeAllLocalAlbumsAndAssets()); - }); - - test('one selected albums, two on device', () async { - when(() => backupRepository.getIdsBySelection(BackupSelection.exclude)).thenAnswer((_) async => []); - when( - () => backupRepository.getIdsBySelection(BackupSelection.select), - ).thenAnswer((_) async => [AlbumStub.oneAsset.localId!]); - when(() => albumMediaRepository.getAll()).thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]); - when(() => syncService.syncLocalAlbumAssetsToDb(any(), any())).thenAnswer((_) async => true); - final result = await sut.refreshDeviceAlbums(); - expect(result, true); - verify(() => syncService.syncLocalAlbumAssetsToDb([AlbumStub.oneAsset], null)).called(1); - verifyNoMoreInteractions(syncService); - }); - }); - - group('refreshRemoteAlbums', () { - test('is working', () async { - when(() => syncService.getUsersFromServer()).thenAnswer((_) async => []); - when(() => syncService.syncUsersFromServer(any())).thenAnswer((_) async => true); - when(() => albumApiRepository.getAll(shared: true)).thenAnswer((_) async => [AlbumStub.sharedWithUser]); - - when( - () => albumApiRepository.getAll(shared: null), - ).thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]); - - when( - () => syncService.syncRemoteAlbumsToDb([AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]), - ).thenAnswer((_) async => true); - final result = await sut.refreshRemoteAlbums(); - expect(result, true); - verify(() => syncService.getUsersFromServer()).called(1); - verify(() => syncService.syncUsersFromServer([])).called(1); - verify(() => albumApiRepository.getAll(shared: true)).called(1); - verify(() => albumApiRepository.getAll(shared: null)).called(1); - verify( - () => syncService.syncRemoteAlbumsToDb([AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]), - ).called(1); - verifyNoMoreInteractions(userService); - verifyNoMoreInteractions(albumApiRepository); - verifyNoMoreInteractions(syncService); - }); - }); - - group('createAlbum', () { - test('shared with assets', () async { - when( - () => albumApiRepository.create( - "name", - assetIds: any(named: "assetIds"), - sharedUserIds: any(named: "sharedUserIds"), - ), - ).thenAnswer((_) async => AlbumStub.oneAsset); - - when( - () => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset), - ).thenAnswer((_) async => AlbumStub.oneAsset); - - when(() => albumRepository.create(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.twoAsset); - - final result = await sut.createAlbum("name", [AssetStub.image1], [UserStub.user1]); - expect(result, AlbumStub.twoAsset); - verify( - () => albumApiRepository.create( - "name", - assetIds: [AssetStub.image1.remoteId!], - sharedUserIds: [UserStub.user1.id], - ), - ).called(1); - verify(() => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset)).called(1); - }); - }); - - group('addAdditionalAssetToAlbum', () { - test('one added, one duplicate', () async { - when( - () => albumApiRepository.addAssets(AlbumStub.oneAsset.remoteId!, any()), - ).thenAnswer((_) async => (added: [AssetStub.image2.remoteId!], duplicates: [AssetStub.image1.remoteId!])); - when(() => albumRepository.get(AlbumStub.oneAsset.id)).thenAnswer((_) async => AlbumStub.oneAsset); - when(() => albumRepository.addAssets(AlbumStub.oneAsset, [AssetStub.image2])).thenAnswer((_) async {}); - when(() => albumRepository.removeAssets(AlbumStub.oneAsset, [])).thenAnswer((_) async {}); - when(() => albumRepository.recalculateMetadata(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.oneAsset); - when(() => albumRepository.update(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.oneAsset); - - final result = await sut.addAssets(AlbumStub.oneAsset, [AssetStub.image1, AssetStub.image2]); - - expect(result != null, true); - expect(result!.alreadyInAlbum, [AssetStub.image1.remoteId!]); - expect(result.successfullyAdded, 1); - }); - }); - - group('addAdditionalUserToAlbum', () { - test('one added', () async { - when( - () => albumApiRepository.addUsers(AlbumStub.emptyAlbum.remoteId!, any()), - ).thenAnswer((_) async => AlbumStub.sharedWithUser); - - when( - () => albumRepository.addUsers( - AlbumStub.emptyAlbum, - AlbumStub.emptyAlbum.sharedUsers.map((u) => u.toDto()).toList(), - ), - ).thenAnswer((_) async => AlbumStub.emptyAlbum); - - when(() => albumRepository.update(AlbumStub.emptyAlbum)).thenAnswer((_) async => AlbumStub.emptyAlbum); - - final result = await sut.addUsers(AlbumStub.emptyAlbum, [UserStub.user2.id]); - - expect(result, true); - }); - }); -} diff --git a/mobile/test/services/asset.service_test.dart b/mobile/test/services/asset.service_test.dart deleted file mode 100644 index b741150165..0000000000 --- a/mobile/test/services/asset.service_test.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; - -import '../api.mocks.dart'; -import '../domain/service.mock.dart'; -import '../fixtures/asset.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../repository.mocks.dart'; -import '../service.mocks.dart'; - -class FakeAssetBulkUpdateDto extends Fake implements AssetBulkUpdateDto {} - -void main() { - late AssetService sut; - - late MockAssetRepository assetRepository; - late MockAssetApiRepository assetApiRepository; - late MockExifInfoRepository exifInfoRepository; - late MockETagRepository eTagRepository; - late MockBackupAlbumRepository backupAlbumRepository; - late MockIsarUserRepository userRepository; - late MockAssetMediaRepository assetMediaRepository; - late MockApiService apiService; - - late MockSyncService syncService; - late MockAlbumService albumService; - late MockBackupService backupService; - late MockUserService userService; - - setUp(() { - assetRepository = MockAssetRepository(); - assetApiRepository = MockAssetApiRepository(); - exifInfoRepository = MockExifInfoRepository(); - userRepository = MockIsarUserRepository(); - eTagRepository = MockETagRepository(); - backupAlbumRepository = MockBackupAlbumRepository(); - apiService = MockApiService(); - assetMediaRepository = MockAssetMediaRepository(); - - syncService = MockSyncService(); - userService = MockUserService(); - albumService = MockAlbumService(); - backupService = MockBackupService(); - - sut = AssetService( - assetApiRepository, - assetRepository, - exifInfoRepository, - userRepository, - eTagRepository, - backupAlbumRepository, - apiService, - syncService, - backupService, - albumService, - userService, - assetMediaRepository, - ); - - registerFallbackValue(FakeAssetBulkUpdateDto()); - }); - - group("Edit ExifInfo", () { - late AssetsApi assetsApi; - setUp(() { - assetsApi = MockAssetsApi(); - when(() => apiService.assetsApi).thenReturn(assetsApi); - when(() => assetsApi.updateAssets(any())).thenAnswer((_) async => Future.value()); - }); - - test("asset is updated with DateTime", () async { - final assets = [AssetStub.image1, AssetStub.image2]; - final dateTime = DateTime.utc(2025, 6, 4, 2, 57); - await sut.changeDateTime(assets, dateTime.toIso8601String()); - - verify(() => assetsApi.updateAssets(any())).called(1); - final upsertExifCallback = verify(() => syncService.upsertAssetsWithExif(captureAny())); - upsertExifCallback.called(1); - final receivedAssets = upsertExifCallback.captured.firstOrNull as List? ?? []; - final receivedDatetime = receivedAssets.cast().map((a) => a.exifInfo?.dateTimeOriginal ?? DateTime(0)); - expect(receivedDatetime.every((d) => d == dateTime), isTrue); - }); - - test("asset is updated with LatLng", () async { - final assets = [AssetStub.image1, AssetStub.image2]; - final latLng = const LatLng(37.7749, -122.4194); - await sut.changeLocation(assets, latLng); - - verify(() => assetsApi.updateAssets(any())).called(1); - final upsertExifCallback = verify(() => syncService.upsertAssetsWithExif(captureAny())); - upsertExifCallback.called(1); - final receivedAssets = upsertExifCallback.captured.firstOrNull as List? ?? []; - final receivedCoords = receivedAssets.cast().map( - (a) => LatLng(a.exifInfo?.latitude ?? 0, a.exifInfo?.longitude ?? 0), - ); - expect(receivedCoords.every((l) => l == latLng), isTrue); - }); - }); -} diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart index 7c7de3cd0e..f9a6d5e282 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -1,18 +1,19 @@ +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; -import 'package:isar/isar.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; import '../domain/service.mock.dart'; import '../repository.mocks.dart'; import '../service.mocks.dart'; -import '../test_utils.dart'; void main() { late AuthService sut; @@ -22,7 +23,7 @@ void main() { late MockNetworkService networkService; late MockBackgroundSyncManager backgroundSyncManager; late MockAppSettingService appSettingsService; - late Isar db; + late Drift db; setUp(() async { authApiRepository = MockAuthApiRepository(); @@ -45,19 +46,16 @@ void main() { }); setUpAll(() async { - db = await TestUtils.initIsar(); - db.writeTxnSync(() => db.clearSync()); - await StoreService.init(storeRepository: IsarStoreRepository(db)); + WidgetsFlutterBinding.ensureInitialized(); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); + }); + + tearDownAll(() async { + await db.close(); }); group('validateServerUrl', () { - setUpAll(() async { - WidgetsFlutterBinding.ensureInitialized(); - final db = await TestUtils.initIsar(); - db.writeTxnSync(() => db.clearSync()); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - }); - test('Should resolve HTTP endpoint', () async { const testUrl = 'http://ip:2283'; const resolvedUrl = 'http://ip:2283/api'; diff --git a/mobile/test/services/entity.service_test.dart b/mobile/test/services/entity.service_test.dart deleted file mode 100644 index 64b9fc604b..0000000000 --- a/mobile/test/services/entity.service_test.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../fixtures/asset.stub.dart'; -import '../fixtures/user.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../repository.mocks.dart'; - -void main() { - late EntityService sut; - late MockAssetRepository assetRepository; - late MockIsarUserRepository userRepository; - - setUp(() { - assetRepository = MockAssetRepository(); - userRepository = MockIsarUserRepository(); - sut = EntityService(assetRepository, userRepository); - }); - - group('fillAlbumWithDatabaseEntities', () { - test('remote album with owner, thumbnail, sharedUsers and assets', () async { - final Album album = - Album( - name: "album-with-two-assets-and-two-users", - localId: "album-with-two-assets-and-two-users-local", - remoteId: "album-with-two-assets-and-two-users-remote", - createdAt: DateTime(2001), - modifiedAt: DateTime(2010), - shared: true, - activityEnabled: true, - startDate: DateTime(2019), - endDate: DateTime(2020), - ) - ..remoteThumbnailAssetId = AssetStub.image1.remoteId - ..assets.addAll([AssetStub.image1, AssetStub.image1]) - ..owner.value = User.fromDto(UserStub.user1) - ..sharedUsers.addAll([User.fromDto(UserStub.admin), User.fromDto(UserStub.admin)]); - - when(() => userRepository.getByUserId(any())).thenAnswer((_) async => UserStub.admin); - when(() => userRepository.getByUserId(any())).thenAnswer((_) async => UserStub.admin); - - when(() => assetRepository.getByRemoteId(AssetStub.image1.remoteId!)).thenAnswer((_) async => AssetStub.image1); - - when(() => userRepository.getByUserIds(any())).thenAnswer((_) async => [UserStub.user1, UserStub.user2]); - - when(() => assetRepository.getAllByRemoteId(any())).thenAnswer((_) async => [AssetStub.image1, AssetStub.image2]); - - await sut.fillAlbumWithDatabaseEntities(album); - expect(album.owner.value?.toDto(), UserStub.admin); - expect(album.thumbnail.value, AssetStub.image1); - expect(album.remoteUsers.map((u) => u.toDto()).toSet(), {UserStub.user1, UserStub.user2}); - expect(album.remoteAssets.toSet(), {AssetStub.image1, AssetStub.image2}); - }); - - test('remote album without any info', () async { - makeEmptyAlbum() => Album( - name: "album-without-info", - localId: "album-without-info-local", - remoteId: "album-without-info-remote", - createdAt: DateTime(2001), - modifiedAt: DateTime(2010), - shared: false, - activityEnabled: false, - ); - - final album = makeEmptyAlbum(); - await sut.fillAlbumWithDatabaseEntities(album); - verifyNoMoreInteractions(assetRepository); - verifyNoMoreInteractions(userRepository); - expect(album, makeEmptyAlbum()); - }); - }); -} diff --git a/mobile/test/services/hash_service_test.dart b/mobile/test/services/hash_service_test.dart deleted file mode 100644 index 9429d434b0..0000000000 --- a/mobile/test/services/hash_service_test.dart +++ /dev/null @@ -1,349 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:file/memory.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/hash.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../fixtures/asset.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../service.mocks.dart'; -import '../mocks/asset_entity.mock.dart'; - -class MockAsset extends Mock implements Asset {} - -void main() { - late HashService sut; - late BackgroundService mockBackgroundService; - late IsarDeviceAssetRepository mockDeviceAssetRepository; - - setUp(() { - mockBackgroundService = MockBackgroundService(); - mockDeviceAssetRepository = MockDeviceAssetRepository(); - - sut = HashService(deviceAssetRepository: mockDeviceAssetRepository, backgroundService: mockBackgroundService); - - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - }); - when(() => mockDeviceAssetRepository.updateAll(any())).thenAnswer((_) async => true); - when(() => mockDeviceAssetRepository.deleteIds(any())).thenAnswer((_) async => true); - }); - - group("HashService: No DeviceAsset entry", () { - test("hash successfully", () async { - final (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); - // No DB entries for this asset - when(() => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!])).thenAnswer((_) async => []); - - final result = await sut.hashAssets([mockAsset]); - - // Verify we stored the new hash in DB - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - verify( - () => mockDeviceAssetRepository.updateAll([ - deviceAsset.copyWith(modifiedTime: AssetStub.image1.fileModifiedAt), - ]), - ).called(1); - verify(() => mockDeviceAssetRepository.deleteIds([])).called(1); - }); - expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]); - }); - }); - - group("HashService: Has DeviceAsset entry", () { - test("when the asset is not modified", () async { - final hash = utf8.encode("image1-hash"); - - when(() => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!])).thenAnswer( - (_) async => [ - DeviceAsset(assetId: AssetStub.image1.localId!, hash: hash, modifiedTime: AssetStub.image1.fileModifiedAt), - ], - ); - final result = await sut.hashAssets([AssetStub.image1]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockBackgroundService.digestFile(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - verifyNever(() => mockDeviceAssetRepository.deleteIds(any())); - - expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]); - }); - - test("hashed successful when asset is modified", () async { - final (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); - when( - () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!]), - ).thenAnswer((_) async => [deviceAsset]); - - final result = await sut.hashAssets([mockAsset]); - - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - verify( - () => mockDeviceAssetRepository.updateAll([ - deviceAsset.copyWith(modifiedTime: AssetStub.image1.fileModifiedAt), - ]), - ).called(1); - verify(() => mockDeviceAssetRepository.deleteIds([])).called(1); - }); - - verify(() => mockBackgroundService.digestFiles([file.path])).called(1); - - expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]); - }); - }); - - group("HashService: Cleanup", () { - late Asset mockAsset; - late Uint8List hash; - late DeviceAsset deviceAsset; - late File file; - - setUp(() async { - (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); - when( - () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!]), - ).thenAnswer((_) async => [deviceAsset]); - }); - - test("cleanups DeviceAsset when local file cannot be obtained", () async { - when(() => mockAsset.local).thenThrow(Exception("File not found")); - final result = await sut.hashAssets([mockAsset]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockBackgroundService.digestFile(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - verify(() => mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!])).called(1); - - expect(result, isEmpty); - }); - - test("cleanups DeviceAsset when hashing failed", () async { - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - - // Verify the callback inside the transaction because, doing it outside results - // in a small delay before the callback is invoked, resulting in other LOCs getting executed - // resulting in an incorrect state - // - // i.e, consider the following piece of code - // await _deviceAssetRepository.transaction(() async { - // await _deviceAssetRepository.updateAll(toBeAdded); - // await _deviceAssetRepository.deleteIds(toBeDeleted); - // }); - // toBeDeleted.clear(); - // since the transaction method is mocked, the callback is not invoked until it is captured - // and executed manually in the next event loop. However, the toBeDeleted.clear() is executed - // immediately once the transaction stub is executed, resulting in the deleteIds method being - // called with an empty list. - // - // To avoid this, we capture the callback and execute it within the transaction stub itself - // and verify the results inside the transaction stub - verify(() => mockDeviceAssetRepository.updateAll([])).called(1); - verify(() => mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!])).called(1); - }); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer( - // Invalid hash, length != 20 - (_) async => [Uint8List.fromList(hash.slice(2).toList())], - ); - - final result = await sut.hashAssets([mockAsset]); - - verify(() => mockBackgroundService.digestFiles([file.path])).called(1); - expect(result, isEmpty); - }); - }); - - group("HashService: Batch processing", () { - test("processes assets in batches when size limit is reached", () async { - // Setup multiple assets with large file sizes - final (mock1, mock2, mock3) = await ( - _createAssetMock(AssetStub.image1), - _createAssetMock(AssetStub.image2), - _createAssetMock(AssetStub.image3), - ).wait; - - final (asset1, file1, deviceAsset1, hash1) = mock1; - final (asset2, file2, deviceAsset2, hash2) = mock2; - final (asset3, file3, deviceAsset3, hash3) = mock3; - - when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); - - // Setup for multiple batch processing calls - when(() => mockBackgroundService.digestFiles([file1.path, file2.path])).thenAnswer((_) async => [hash1, hash2]); - when(() => mockBackgroundService.digestFiles([file3.path])).thenAnswer((_) async => [hash3]); - - final size = await file1.length() + await file2.length(); - - sut = HashService( - deviceAssetRepository: mockDeviceAssetRepository, - backgroundService: mockBackgroundService, - batchSizeLimit: size, - ); - final result = await sut.hashAssets([asset1, asset2, asset3]); - - // Verify multiple batch process calls - verify(() => mockBackgroundService.digestFiles([file1.path, file2.path])).called(1); - verify(() => mockBackgroundService.digestFiles([file3.path])).called(1); - - expect(result, [ - AssetStub.image1.copyWith(checksum: base64.encode(hash1)), - AssetStub.image2.copyWith(checksum: base64.encode(hash2)), - AssetStub.image3.copyWith(checksum: base64.encode(hash3)), - ]); - }); - - test("processes assets in batches when file limit is reached", () async { - // Setup multiple assets with large file sizes - final (mock1, mock2, mock3) = await ( - _createAssetMock(AssetStub.image1), - _createAssetMock(AssetStub.image2), - _createAssetMock(AssetStub.image3), - ).wait; - - final (asset1, file1, deviceAsset1, hash1) = mock1; - final (asset2, file2, deviceAsset2, hash2) = mock2; - final (asset3, file3, deviceAsset3, hash3) = mock3; - - when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); - - when(() => mockBackgroundService.digestFiles([file1.path])).thenAnswer((_) async => [hash1]); - when(() => mockBackgroundService.digestFiles([file2.path])).thenAnswer((_) async => [hash2]); - when(() => mockBackgroundService.digestFiles([file3.path])).thenAnswer((_) async => [hash3]); - - sut = HashService( - deviceAssetRepository: mockDeviceAssetRepository, - backgroundService: mockBackgroundService, - batchFileLimit: 1, - ); - final result = await sut.hashAssets([asset1, asset2, asset3]); - - // Verify multiple batch process calls - verify(() => mockBackgroundService.digestFiles([file1.path])).called(1); - verify(() => mockBackgroundService.digestFiles([file2.path])).called(1); - verify(() => mockBackgroundService.digestFiles([file3.path])).called(1); - - expect(result, [ - AssetStub.image1.copyWith(checksum: base64.encode(hash1)), - AssetStub.image2.copyWith(checksum: base64.encode(hash2)), - AssetStub.image3.copyWith(checksum: base64.encode(hash3)), - ]); - }); - - test("HashService: Sort & Process different states", () async { - final (asset1, file1, deviceAsset1, hash1) = await _createAssetMock(AssetStub.image1); // Will need rehashing - final (asset2, file2, deviceAsset2, hash2) = await _createAssetMock(AssetStub.image2); // Will have matching hash - final (asset3, file3, deviceAsset3, hash3) = await _createAssetMock(AssetStub.image3); // No DB entry - final asset4 = AssetStub.image3.copyWith(localId: "image4"); // Cannot be hashed - - when(() => mockBackgroundService.digestFiles([file1.path, file3.path])).thenAnswer((_) async => [hash1, hash3]); - // DB entries are not sorted and a dummy entry added - when( - () => mockDeviceAssetRepository.getByIds([ - AssetStub.image1.localId!, - AssetStub.image2.localId!, - AssetStub.image3.localId!, - asset4.localId!, - ]), - ).thenAnswer( - (_) async => [ - // Same timestamp to reuse deviceAsset - deviceAsset2.copyWith(modifiedTime: asset2.fileModifiedAt), - deviceAsset1, - deviceAsset3.copyWith(assetId: asset4.localId!), - ], - ); - - final result = await sut.hashAssets([asset1, asset2, asset3, asset4]); - - // Verify correct processing of all assets - verify(() => mockBackgroundService.digestFiles([file1.path, file3.path])).called(1); - expect(result.length, 3); - expect(result, [ - AssetStub.image2.copyWith(checksum: base64.encode(hash2)), - AssetStub.image1.copyWith(checksum: base64.encode(hash1)), - AssetStub.image3.copyWith(checksum: base64.encode(hash3)), - ]); - }); - - group("HashService: Edge cases", () { - test("handles empty list of assets", () async { - when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); - - final result = await sut.hashAssets([]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - verifyNever(() => mockDeviceAssetRepository.deleteIds(any())); - - expect(result, isEmpty); - }); - - test("handles all file access failures", () async { - // No DB entries - when( - () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!, AssetStub.image2.localId!]), - ).thenAnswer((_) async => []); - - final result = await sut.hashAssets([AssetStub.image1, AssetStub.image2]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - expect(result, isEmpty); - }); - }); - }); -} - -Future<(Asset, File, DeviceAsset, Uint8List)> _createAssetMock(Asset asset) async { - final random = Random(); - final hash = Uint8List.fromList(List.generate(20, (i) => random.nextInt(255))); - final mockAsset = MockAsset(); - final mockAssetEntity = MockAssetEntity(); - final fs = MemoryFileSystem(); - final deviceAsset = DeviceAsset( - assetId: asset.localId!, - hash: Uint8List.fromList(hash), - modifiedTime: DateTime.now(), - ); - final tmp = await fs.systemTempDirectory.createTemp(); - final file = tmp.childFile("${asset.fileName}-path"); - await file.writeAsString("${asset.fileName}-content"); - - when(() => mockAsset.localId).thenReturn(asset.localId); - when(() => mockAsset.fileName).thenReturn(asset.fileName); - when(() => mockAsset.fileCreatedAt).thenReturn(asset.fileCreatedAt); - when(() => mockAsset.fileModifiedAt).thenReturn(asset.fileModifiedAt); - when( - () => mockAsset.copyWith(checksum: any(named: "checksum")), - ).thenReturn(asset.copyWith(checksum: base64.encode(hash))); - when(() => mockAsset.local).thenAnswer((_) => mockAssetEntity); - when(() => mockAssetEntity.originFile).thenAnswer((_) async => file); - - return (mockAsset, file, deviceAsset, hash); -} diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart index 30d4e2e6d4..75a41b46fb 100644 --- a/mobile/test/test_utils.dart +++ b/mobile/test/test_utils.dart @@ -4,82 +4,13 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:fake_async/fake_async.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as domain; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; import 'mock_http_override.dart'; -// Listener Mock to test when a provider notifies its listeners -class ListenerMock extends Mock { - void call(T? previous, T next); -} - abstract final class TestUtils { const TestUtils._(); - /// Downloads Isar binaries (if required) and initializes a new Isar db - static Future initIsar() async { - await Isar.initializeIsarCore(download: true); - - final instance = Isar.getInstance(); - if (instance != null) { - return instance; - } - - final db = await Isar.open( - [ - StoreValueSchema, - ExifInfoSchema, - AssetSchema, - AlbumSchema, - UserSchema, - BackupAlbumSchema, - DuplicatedAssetSchema, - ETagSchema, - AndroidDeviceAssetSchema, - IOSDeviceAssetSchema, - DeviceAssetEntitySchema, - ], - directory: "test/", - maxSizeMiB: 1024, - inspector: false, - ); - - // Clear and close db on test end - addTearDown(() async { - await db.writeTxn(() async => await db.clear()); - await db.close(); - }); - return db; - } - - /// Creates a new ProviderContainer to test Riverpod providers - static ProviderContainer createContainer({ - ProviderContainer? parent, - List overrides = const [], - List? observers, - }) { - final container = ProviderContainer(parent: parent, overrides: overrides, observers: observers); - - // Dispose on test end - addTearDown(container.dispose); - - return container; - } - static void init() { // Turn off easy localization logging EasyLocalization.logger.enableBuildModes = []; diff --git a/mobile/test/test_utils/medium_factory.dart b/mobile/test/test_utils/medium_factory.dart index 50e73e5b5e..c8c41bbf0f 100644 --- a/mobile/test/test_utils/medium_factory.dart +++ b/mobile/test/test_utils/medium_factory.dart @@ -1,7 +1,6 @@ import 'dart:math'; 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/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; @@ -10,28 +9,6 @@ class MediumFactory { const MediumFactory(Drift db) : _db = db; - LocalAsset localAsset({ - String? id, - String? name, - AssetType? type, - DateTime? createdAt, - DateTime? updatedAt, - String? checksum, - }) { - final random = Random(); - - return LocalAsset( - id: id ?? '${random.nextInt(1000000)}', - name: name ?? 'Asset ${random.nextInt(1000000)}', - checksum: checksum ?? '${random.nextInt(1000000)}', - type: type ?? AssetType.image, - createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), - updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), - playbackStyle: AssetPlaybackStyle.image, - isEdited: false, - ); - } - LocalAlbum localAlbum({ String? id, String? name, diff --git a/mobile/test/utils/action_button_utils_test.dart b/mobile/test/utils/action_button_utils_test.dart index b5540f9dc7..9956dfa2d0 100644 --- a/mobile/test/utils/action_button_utils_test.dart +++ b/mobile/test/utils/action_button_utils_test.dart @@ -727,7 +727,7 @@ void main() { expect(ActionButtonType.setAlbumCover.shouldShow(context), isTrue); }); - test('should not show when not owner', () { + test('should show when not owner', () { final album = createRemoteAlbum(); final context = ActionButtonContext( asset: mergedAsset, @@ -742,7 +742,7 @@ void main() { selectedCount: 1, ); - expect(ActionButtonType.setAlbumCover.shouldShow(context), isFalse); + expect(ActionButtonType.setAlbumCover.shouldShow(context), isTrue); }); test('should not show when in locked view', () { diff --git a/mobile/test/utils/editor_test.dart b/mobile/test/utils/editor_test.dart new file mode 100644 index 0000000000..16f1c08d05 --- /dev/null +++ b/mobile/test/utils/editor_test.dart @@ -0,0 +1,322 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/utils/editor.utils.dart'; +import 'package:openapi/api.dart' show MirrorAxis, MirrorParameters, RotateParameters; + +List normalizedToEdits(NormalizedTransform transform) { + List edits = []; + + if (transform.mirrorHorizontal) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal))); + } + + if (transform.mirrorVertical) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical))); + } + + if (transform.rotation != 0) { + edits.add(RotateEdit(RotateParameters(angle: transform.rotation))); + } + + return edits; +} + +bool compareEditAffines(List editsA, List editsB) { + final normA = buildAffineFromEdits(editsA); + final normB = buildAffineFromEdits(editsB); + + return ((normA.a - normB.a).abs() < 0.0001 && + (normA.b - normB.b).abs() < 0.0001 && + (normA.c - normB.c).abs() < 0.0001 && + (normA.d - normB.d).abs() < 0.0001); +} + +void main() { + group('normalizeEdits', () { + test('should handle no edits', () { + final edits = []; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single 90° rotation', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single 180° rotation', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single 270° rotation', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single horizontal mirror', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single vertical mirror', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 90° rotation + horizontal mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 90° rotation + vertical mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 90° rotation + both mirrors', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 180° rotation + horizontal mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 180° rotation + vertical mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 180° rotation + both mirrors', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 270° rotation + horizontal mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 270° rotation + vertical mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 270° rotation + both mirrors', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle horizontal mirror + 90° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle horizontal mirror + 180° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle horizontal mirror + 270° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle vertical mirror + 90° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle vertical mirror + 180° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle vertical mirror + 270° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle both mirrors + 90° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle both mirrors + 180° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle both mirrors + 270° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + }); +} diff --git a/open-api/bin/generate-open-api.sh b/open-api/bin/generate-open-api.sh index 522063185f..9d7b158fc3 100755 --- a/open-api/bin/generate-open-api.sh +++ b/open-api/bin/generate-open-api.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash OPENAPI_GENERATOR_VERSION=v7.12.0 +set -euo pipefail + # usage: ./bin/generate-open-api.sh function dart { @@ -15,12 +17,13 @@ function dart { patch --no-backup-if-mismatch -u api.mustache