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/workflows/auto-close.yml b/.github/workflows/auto-close.yml index 348c0785bb..aa5d41ff98 100644 --- a/.github/workflows/auto-close.yml +++ b/.github/workflows/auto-close.yml @@ -30,7 +30,7 @@ jobs: 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" + echo "uses_template=$OK" | tee --append "$GITHUB_OUTPUT" close_template: runs-on: ubuntu-latest @@ -51,7 +51,7 @@ jobs: 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 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: { @@ -128,7 +128,7 @@ jobs: run: | REMAINING=$(gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" --json labels \ --jq '[.labels[].name | select(startswith("auto-closed:"))] | length') - echo "remaining=$REMAINING" >> "$GITHUB_OUTPUT" + echo "remaining=$REMAINING" | tee --append "$GITHUB_OUTPUT" - name: Reopen PR if: ${{ steps.check_labels.outputs.remaining == '0' }} diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 7c106415d8..c2a3918cfe 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -121,7 +121,7 @@ jobs: 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,7 +153,7 @@ jobs: fi - name: Publish Android Artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: release-apk-signed path: mobile/build/app/outputs/flutter-apk/*.apk @@ -210,7 +210,7 @@ jobs: working-directory: ./mobile - name: Setup Ruby - uses: ruby/setup-ruby@c515ec17f69368147deb311832da000dd229d338 # v1.297.0 + 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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/check-openapi.yml b/.github/workflows/check-openapi.yml index cd05f320c6..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@1f38ea5ea0b4a2e4e49901c3bcdf4386a05e9ea1 # v0.0.37 + 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/cli.yml b/.github/workflows/cli.yml index 9d08d3f816..2a334af89d 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -89,7 +89,7 @@ jobs: uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Login to GitHub Container Registry - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 if: ${{ !github.event.pull_request.head.repo.fork }} with: registry: ghcr.io @@ -115,7 +115,7 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'release' }} - name: Build and push image - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + 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 b73ba5e634..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:df7188ba88abb0800d73cc97d3633280f0c0c3d4c441d678225067bf154150fb + image: ghcr.io/immich-app/mdq:main@sha256:557cca601891b8b7d78b940071d35aaf7aaeb9b327d19b22cf282118edbc5272 outputs: checked: ${{ steps.get_checkbox.outputs.checked }} steps: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index adeacbf271..2378b032b6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -57,7 +57,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + 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@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + 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@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + 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 d5c327b9e7..84509103be 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -60,7 +60,7 @@ jobs: suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn'] steps: - name: Login to GitHub Container Registry - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 2055bfce65..0ccebfb363 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -86,7 +86,7 @@ jobs: run: pnpm build - name: Upload build output - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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 05c845ccd1..8aa063e1bb 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -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: @@ -135,7 +135,7 @@ jobs: - 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/fix-format.yml b/.github/workflows/fix-format.yml index ae8e0b29ca..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@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + 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@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.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 fcda857eda..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@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + 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/prepare-release.yml b/.github/workflows/prepare-release.yml index dec9b06d67..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@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + 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@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - name: Setup pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.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@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + 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/preview-label.yaml b/.github/workflows/preview-label.yaml index 43c971c31b..5cf0008597 100644 --- a/.github/workflows/preview-label.yaml +++ b/.github/workflows/preview-label.yaml @@ -19,7 +19,7 @@ jobs: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3.9.0 + - uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0 with: github-token: ${{ steps.token.outputs.token }} message-id: 'preview-status' @@ -37,7 +37,7 @@ jobs: 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@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3.9.0 + - 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@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3.9.0 + - 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/test.yml b/.github/workflows/test.yml index a606aba124..4558b90866 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: always() with: name: e2e-server-docker-logs-${{ matrix.runner }} @@ -522,7 +522,7 @@ jobs: run: pnpm test:web if: ${{ !cancelled() }} - name: Archive e2e test (web) results - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: always() with: name: e2e-web-docker-logs-${{ matrix.runner }} @@ -620,7 +620,7 @@ jobs: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Install uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: python-version: 3.11 - name: Install dependencies 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/package.json b/cli/package.json index 4b7da12ebc..108e65f945 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.6.3", + "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.12.0", + "@types/node": "^24.12.2", "@vitest/coverage-v8": "^4.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -28,13 +28,13 @@ "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", + "typescript": "^6.0.0", + "typescript-eslint": "^8.58.0", "vite": "^8.0.0", "vitest": "^4.0.0", "vitest-fetch-mock": "^0.4.0", 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/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/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 41068dee97..b29c233153 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -37,7 +37,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N | `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, 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 | diff --git a/docs/docs/install/requirements.md b/docs/docs/install/requirements.md index 178cf45388..66f3033a43 100644 --- a/docs/docs/install/requirements.md +++ b/docs/docs/install/requirements.md @@ -49,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 03dae60b33..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": [ diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index afaa584882..964291ad08 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "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" 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/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 d545fa1b86..6b72c1b36d 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "2.6.3", + "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.12.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,7 +51,7 @@ "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", diff --git a/e2e/src/specs/server/api/album.e2e-spec.ts b/e2e/src/specs/server/api/album.e2e-spec.ts index a9e90940ab..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, 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/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 index 34f11cdf78..c39e9019d3 100644 --- a/e2e/src/specs/web/duplicates.e2e-spec.ts +++ b/e2e/src/specs/web/duplicates.e2e-spec.ts @@ -16,8 +16,8 @@ test.describe('Duplicates Utility', () => { test.beforeEach(async ({ context }) => { [firstAsset, secondAsset] = await Promise.all([ - utils.createAsset(admin.accessToken, { deviceAssetId: 'duplicate-a' }), - utils.createAsset(admin.accessToken, { deviceAssetId: 'duplicate-b' }), + utils.createAsset(admin.accessToken, {}), + utils.createAsset(admin.accessToken, {}), ]); await updateAssets( 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/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 4d44d99e2f..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) }); }, 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 1025548cf5..cc30d9e350 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", @@ -1389,6 +1396,7 @@ "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", @@ -1559,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", @@ -1924,6 +1934,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", @@ -2212,9 +2224,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", 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 verwijderd.", "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 a66505923d..2b9548ed8b 100644 --- a/i18n/package.json +++ b/i18n/package.json @@ -1,6 +1,6 @@ { "name": "immich-i18n", - "version": "2.6.3", + "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": "Omoguć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": "Omogućite/onemoguć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": "启用哈希校验", "storage_template_hash_verification_enabled_description": "开启哈希校验功能。若不清楚关闭的后果,请勿关闭", "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": "为获得最佳的后台备份体验,请在系统设置中禁用针对 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_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码验证", "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": "请进行身份验证以访问", "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": "验证", "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 fd6b61d6c2..640996f54a 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "immich-ml" -version = "2.6.3" +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 e07f942312..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.3" +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 90cc03a50f..c4700fd924 100644 --- a/mise.toml +++ b/mise.toml @@ -15,9 +15,9 @@ config_roots = [ [tools] node = "24.14.1" -flutter = "3.35.7" -pnpm = "10.32.1" -terragrunt = "0.99.4" +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..1bb94819e5 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.6", "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 cefdf4fbd2..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 @@ -53,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" 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/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 279b985cfe..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" => 3041, - "android.injected.version.name" => "2.6.3", + "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 9eb93f9ff9..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] = [] 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 1bf52807f9..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.3 + 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/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/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_people_collection.page.dart b/mobile/lib/presentation/pages/drift_people_collection.page.dart index d34ce3e776..32bbd7e60b 100644 --- a/mobile/lib/presentation/pages/drift_people_collection.page.dart +++ b/mobile/lib/presentation/pages/drift_people_collection.page.dart @@ -89,7 +89,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 452f6cc1d5..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,24 +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 hasRequestedSearch = useState(false); final dateInputFilter = useState(null); final peopleCurrentFilterWidget = useState(null); @@ -83,68 +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; - hasRequestedSearch.value = false; - return; + if (!f.isEmpty) { + unawaited(ref.read(paginatedSearchProvider.notifier).search(f)); } - - hasRequestedSearch.value = true; - 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, () { - filter.value = preFilter; - textSearchController.clear(); - 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( @@ -155,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), ), @@ -164,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( @@ -191,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()), ), @@ -200,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( @@ -234,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), @@ -251,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( @@ -275,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), @@ -288,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 { @@ -362,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); }, ), @@ -376,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), ), @@ -409,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( @@ -429,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), ), @@ -438,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, @@ -648,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), ), @@ -724,7 +683,7 @@ class DriftSearchPage extends HookConsumerWidget { ), ), ), - if (!hasRequestedSearch.value) + if (filter.value.isEmpty) const _SearchSuggestions() else _SearchResultGrid(onScrollEnd: loadMoreSearchResults), 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/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 bb42140d0a..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'; 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 b51960bb05..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) ...[ 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/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 b05a28172d..b6b08d7831 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; @@ -28,21 +27,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/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/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 8e810ced2a..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); @@ -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/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/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 ff782113c7..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,35 +72,30 @@ 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( @@ -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, shadows: _controlShadows) - : AnimatedPlayPause(color: Colors.white, 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,7 +125,7 @@ class VideoControls extends HookConsumerWidget { color: Colors.white, fontWeight: FontWeight.w500, fontFeatures: [FontFeature.tabularFigures()], - shadows: _controlShadows, + shadows: VideoControls._controlShadows, ), ), const SizedBox(width: 12), @@ -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/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..4d20a0a149 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -1,5 +1,5 @@ [tools] -flutter = "3.35.7" +flutter = "3.41.6" [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 e79a60f98b..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.3 +- 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,12 +142,7 @@ 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 @@ -239,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 @@ -267,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 @@ -330,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) @@ -349,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) @@ -356,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) @@ -370,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) @@ -388,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) @@ -404,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) @@ -440,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) @@ -504,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) @@ -545,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 6b554fb644..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'; @@ -178,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'; @@ -242,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'; @@ -283,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/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 48e5f5874b..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': @@ -402,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': @@ -530,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': @@ -612,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_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_response_dto.dart b/mobile/openapi/lib/model/bulk_id_response_dto.dart index 1fa8536964..bb3f1d8856 100644 --- a/mobile/openapi/lib/model/bulk_id_response_dto.dart +++ b/mobile/openapi/lib/model/bulk_id_response_dto.dart @@ -19,8 +19,13 @@ class BulkIdResponseDto { 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 @@ -80,7 +85,7 @@ 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')!, @@ -136,86 +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'); - static const validation = BulkIdResponseDtoErrorEnum._(r'validation'); - - /// List of all possible values in this [enum][BulkIdResponseDtoErrorEnum]. - static const values = [ - duplicate, - noPermission, - notFound, - unknown, - validation, - ]; - - 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; - case r'validation': return BulkIdResponseDtoErrorEnum.validation; - 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/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 4dbc90d407..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 @@ -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 d5803c9cc7..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 @@ -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_theme_dto.dart b/mobile/openapi/lib/model/server_theme_dto.dart deleted file mode 100644 index 957cf84d55..0000000000 --- a/mobile/openapi/lib/model/server_theme_dto.dart +++ /dev/null @@ -1,100 +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 ServerThemeDto { - /// Returns a new [ServerThemeDto] instance. - ServerThemeDto({ - required this.customCss, - }); - - /// Custom CSS for theming - String customCss; - - @override - bool operator ==(Object other) => identical(this, other) || other is ServerThemeDto && - other.customCss == customCss; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (customCss.hashCode); - - @override - String toString() => 'ServerThemeDto[customCss=$customCss]'; - - Map toJson() { - final json = {}; - json[r'customCss'] = this.customCss; - return json; - } - - /// Returns a new [ServerThemeDto] 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"); - if (value is Map) { - final json = value.cast(); - - return ServerThemeDto( - customCss: mapValueOfType(json, r'customCss')!, - ); - } - 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 = ServerThemeDto.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 = ServerThemeDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - 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 = >{}; - 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,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'customCss', - }; -} - 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..8fc2cfe030 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.6" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index c5839fb9be..b37c9fd23f 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.3+3041 +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.6 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/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/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